Support partial sensor readings and improve room metrics aggregation
- Allow room and card components to handle rooms with missing energy or CO2 data - Update RoomMetrics type to make energy and co2 fields optional - Track which sensors provide energy or CO2 data per room - Aggregate room metrics only from available data (partial readings) - Update AirQualityCard and RoomMetricsCard to safely access optional fields - Set MAX_HISTORY_POINTS to 48 in energy store - Improve robustness of API room fetching and data mapping - Update CLAUDE.md with new partial reading support and data flow details
This commit is contained in:
@@ -31,16 +31,16 @@
|
||||
|
||||
<div v-for="room in roomsList" :key="room.room" class="flex items-center justify-between p-2 rounded">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
:class="getCO2StatusColor(room.co2.status)"
|
||||
:class="getCO2StatusColor(room.co2?.status || 'good')"
|
||||
></div>
|
||||
<span class="text-sm font-medium text-gray-900">{{ room.room }}</span>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-sm text-gray-900">{{ Math.round(room.co2.current) }} ppm</div>
|
||||
<div class="text-xs" :class="getCO2TextColor(room.co2.status)">
|
||||
{{ room.co2.status.toUpperCase() }}
|
||||
<div class="text-sm text-gray-900">{{ Math.round(room.co2?.current || 0) }} ppm</div>
|
||||
<div class="text-xs" :class="getCO2TextColor(room.co2?.status || 'good')">
|
||||
{{ (room.co2?.status || 'good').toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,14 +75,17 @@ import { useRoomStore } from '@/stores/room'
|
||||
const roomStore = useRoomStore()
|
||||
|
||||
const roomsList = computed(() => {
|
||||
return Array.from(roomStore.roomsData.values()).sort((a, b) =>
|
||||
b.co2.current - a.co2.current // Sort by CO2 level descending
|
||||
)
|
||||
return Array.from(roomStore.roomsData.values())
|
||||
.filter(room => room.co2) // Only include rooms with CO2 data
|
||||
.sort((a, b) =>
|
||||
(b.co2?.current || 0) - (a.co2?.current || 0) // Sort by CO2 level descending
|
||||
)
|
||||
})
|
||||
|
||||
const overallCO2 = computed(() => {
|
||||
if (roomsList.value.length === 0) return 0
|
||||
return roomsList.value.reduce((sum, room) => sum + room.co2.current, 0) / roomsList.value.length
|
||||
const total = roomsList.value.reduce((sum, room) => sum + (room.co2?.current || 0), 0)
|
||||
return total / roomsList.value.length
|
||||
})
|
||||
|
||||
const overallStatus = computed(() => {
|
||||
@@ -90,18 +93,18 @@ const overallStatus = computed(() => {
|
||||
})
|
||||
|
||||
const roomsWithGoodAir = computed(() => {
|
||||
return roomsList.value.filter(room => room.co2.status === 'good').length
|
||||
return roomsList.value.filter(room => room.co2?.status === 'good').length
|
||||
})
|
||||
|
||||
const roomsNeedingAttention = computed(() => {
|
||||
return roomsList.value.filter(room => ['poor', 'critical'].includes(room.co2.status)).length
|
||||
return roomsList.value.filter(room => room.co2?.status && ['poor', 'critical'].includes(room.co2.status)).length
|
||||
})
|
||||
|
||||
const recommendations = computed(() => {
|
||||
const recs = []
|
||||
const criticalRooms = roomsList.value.filter(room => room.co2.status === 'critical')
|
||||
const poorRooms = roomsList.value.filter(room => room.co2.status === 'poor')
|
||||
|
||||
const criticalRooms = roomsList.value.filter(room => room.co2?.status === 'critical')
|
||||
const poorRooms = roomsList.value.filter(room => room.co2?.status === 'poor')
|
||||
|
||||
if (criticalRooms.length > 0) {
|
||||
recs.push(`Immediate ventilation needed in ${criticalRooms[0].room}`)
|
||||
}
|
||||
@@ -111,7 +114,7 @@ const recommendations = computed(() => {
|
||||
if (overallCO2.value > 800) {
|
||||
recs.push('Consider adjusting HVAC settings building-wide')
|
||||
}
|
||||
|
||||
|
||||
return recs.slice(0, 3) // Max 3 recommendations
|
||||
})
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
<h3 class="font-medium text-gray-900">{{ room.room }}</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- CO2 Status Indicator -->
|
||||
<div
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
:class="getCO2StatusColor(room.co2.status)"
|
||||
:class="getCO2StatusColor(room.co2!.status)"
|
||||
></div>
|
||||
<!-- Occupancy Indicator -->
|
||||
<div class="flex items-center gap-1 text-xs text-gray-500">
|
||||
@@ -32,15 +32,15 @@
|
||||
<!-- Energy -->
|
||||
<div class="bg-blue-50 rounded p-2">
|
||||
<div class="text-blue-600 font-medium">Energy</div>
|
||||
<div class="text-blue-900">{{ room.energy.current.toFixed(2) }} {{ room.energy.unit }}</div>
|
||||
<div class="text-blue-600 text-xs">Total: {{ room.energy.total.toFixed(2) }}</div>
|
||||
<div class="text-blue-900">{{ room.energy!.current.toFixed(2) }} {{ room.energy!.unit }}</div>
|
||||
<div class="text-blue-600 text-xs">Total: {{ room.energy!.total.toFixed(2) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- CO2 -->
|
||||
<div class="rounded p-2" :class="getCO2BackgroundColor(room.co2.status)">
|
||||
<div class="font-medium" :class="getCO2TextColor(room.co2.status)">CO2</div>
|
||||
<div :class="getCO2TextColor(room.co2.status)">{{ Math.round(room.co2.current) }} {{ room.co2.unit }}</div>
|
||||
<div class="text-xs" :class="getCO2TextColor(room.co2.status)">{{ room.co2.status.toUpperCase() }}</div>
|
||||
<div class="rounded p-2" :class="getCO2BackgroundColor(room.co2!.status)">
|
||||
<div class="font-medium" :class="getCO2TextColor(room.co2!.status)">CO2</div>
|
||||
<div :class="getCO2TextColor(room.co2!.status)">{{ Math.round(room.co2!.current) }} {{ room.co2!.unit }}</div>
|
||||
<div class="text-xs" :class="getCO2TextColor(room.co2!.status)">{{ room.co2!.status.toUpperCase() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -77,18 +77,19 @@ import { useRoomStore } from '@/stores/room'
|
||||
const roomStore = useRoomStore()
|
||||
|
||||
const roomsList = computed(() => {
|
||||
return Array.from(roomStore.roomsData.values()).sort((a, b) =>
|
||||
a.room.localeCompare(b.room)
|
||||
)
|
||||
return Array.from(roomStore.roomsData.values())
|
||||
.filter(room => room.energy && room.co2) // Only show rooms with both metrics
|
||||
.sort((a, b) => a.room.localeCompare(b.room))
|
||||
})
|
||||
|
||||
const totalEnergy = computed(() => {
|
||||
return roomsList.value.reduce((sum, room) => sum + room.energy.current, 0)
|
||||
return roomsList.value.reduce((sum, room) => sum + (room.energy?.current || 0), 0)
|
||||
})
|
||||
|
||||
const averageCO2 = computed(() => {
|
||||
if (roomsList.value.length === 0) return 0
|
||||
return roomsList.value.reduce((sum, room) => sum + room.co2.current, 0) / roomsList.value.length
|
||||
const total = roomsList.value.reduce((sum, room) => sum + (room.co2?.current || 0), 0)
|
||||
return total / roomsList.value.length
|
||||
})
|
||||
|
||||
const getCO2StatusColor = (status: string) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useSensorStore } from './sensor'
|
||||
import { useRoomStore } from './room'
|
||||
import { useAnalyticsStore } from './analytics'
|
||||
|
||||
const MAX_HISTORY_POINTS = 100
|
||||
const MAX_HISTORY_POINTS = 48
|
||||
|
||||
/**
|
||||
* Energy Store - Simplified to only track energy consumption metrics
|
||||
@@ -67,7 +67,7 @@ export const useEnergyStore = defineStore('energy', () => {
|
||||
energyHistory.shift()
|
||||
energyTimestamps.shift()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Update current consumption (called from components or watchers)
|
||||
|
||||
@@ -13,13 +13,15 @@ interface WindowWithAuth extends Window {
|
||||
interface RoomMetrics {
|
||||
room: string
|
||||
sensors: string[]
|
||||
energy: {
|
||||
energySensors: string[] // Track which sensors provide energy data
|
||||
co2Sensors: string[] // Track which sensors provide CO2 data
|
||||
energy?: {
|
||||
current: number
|
||||
total: number
|
||||
average: number
|
||||
unit: string
|
||||
}
|
||||
co2: {
|
||||
co2?: {
|
||||
current: number
|
||||
average: number
|
||||
max: number
|
||||
@@ -44,9 +46,9 @@ export const useRoomStore = defineStore('room', () => {
|
||||
function updateRoomData(data: SensorReading): void {
|
||||
const sensorStore = useSensorStore()
|
||||
|
||||
// Validate data structure and provide fallbacks
|
||||
if (!data.energy || !data.co2) {
|
||||
console.warn('Invalid sensor reading data, missing energy or co2 properties:', data)
|
||||
// Accept partial readings - validate that we have at least room and sensor_id
|
||||
if (!data.room || !data.sensor_id) {
|
||||
console.warn('Invalid sensor reading data, missing room or sensor_id:', data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -57,11 +59,12 @@ export const useRoomStore = defineStore('room', () => {
|
||||
let roomMetrics = roomsData.get(data.room)
|
||||
|
||||
if (!roomMetrics) {
|
||||
// Initialize with minimal required fields - energy and co2 are optional
|
||||
roomMetrics = {
|
||||
room: data.room,
|
||||
sensors: [data.sensor_id],
|
||||
energy: { current: 0, total: 0, average: 0, unit: data.energy?.unit || 'kWh' },
|
||||
co2: { current: 0, average: 0, max: 0, status: 'good', unit: data.co2?.unit || 'ppm' },
|
||||
sensors: [],
|
||||
energySensors: [],
|
||||
co2Sensors: [],
|
||||
occupancyEstimate: 'low',
|
||||
lastUpdated: data.timestamp,
|
||||
}
|
||||
@@ -73,32 +76,78 @@ export const useRoomStore = defineStore('room', () => {
|
||||
roomMetrics.sensors.push(data.sensor_id)
|
||||
}
|
||||
|
||||
// Track which sensors provide which metrics
|
||||
if (data.energy?.value !== undefined && !roomMetrics.energySensors.includes(data.sensor_id)) {
|
||||
roomMetrics.energySensors.push(data.sensor_id)
|
||||
}
|
||||
if (data.co2?.value !== undefined && !roomMetrics.co2Sensors.includes(data.sensor_id)) {
|
||||
roomMetrics.co2Sensors.push(data.sensor_id)
|
||||
}
|
||||
|
||||
// Recalculate room metrics from all sensors in the room
|
||||
const roomSensors = Array.from(sensorStore.latestReadings.values()).filter(
|
||||
(reading) => reading.room === data.room,
|
||||
)
|
||||
|
||||
// Energy calculations
|
||||
roomMetrics.energy.current = roomSensors.reduce((sum, sensor) => sum + sensor.energy.value, 0)
|
||||
roomMetrics.energy.total += data.energy.value // Accumulate total
|
||||
roomMetrics.energy.average = roomMetrics.energy.total / roomSensors.length
|
||||
// Energy calculations - only if energy data is present in ANY sensor
|
||||
const energySensors = roomSensors.filter((sensor) => sensor.energy?.value !== undefined)
|
||||
if (energySensors.length > 0) {
|
||||
// Initialize energy object if it doesn't exist
|
||||
if (!roomMetrics.energy) {
|
||||
roomMetrics.energy = {
|
||||
current: 0,
|
||||
total: 0,
|
||||
average: 0,
|
||||
unit: data.energy?.unit || 'kWh',
|
||||
}
|
||||
}
|
||||
|
||||
// CO2 calculations
|
||||
const co2Values = roomSensors.map((sensor) => sensor.co2.value)
|
||||
roomMetrics.co2.current = co2Values.reduce((sum, val) => sum + val, 0) / co2Values.length
|
||||
roomMetrics.co2.max = Math.max(roomMetrics.co2.max, ...co2Values)
|
||||
roomMetrics.co2.average = (roomMetrics.co2.average + roomMetrics.co2.current) / 2
|
||||
roomMetrics.energy.current = energySensors.reduce(
|
||||
(sum, sensor) => sum + (sensor.energy?.value || 0),
|
||||
0,
|
||||
)
|
||||
if (data.energy?.value !== undefined) {
|
||||
roomMetrics.energy.total += data.energy.value // Accumulate total only for this reading
|
||||
}
|
||||
roomMetrics.energy.average = roomMetrics.energy.total / energySensors.length
|
||||
if (data.energy?.unit) {
|
||||
roomMetrics.energy.unit = data.energy.unit
|
||||
}
|
||||
}
|
||||
|
||||
// CO2 status classification
|
||||
if (roomMetrics.co2.current < 400) roomMetrics.co2.status = 'good'
|
||||
else if (roomMetrics.co2.current < 1000) roomMetrics.co2.status = 'moderate'
|
||||
else if (roomMetrics.co2.current < 5000) roomMetrics.co2.status = 'poor'
|
||||
else roomMetrics.co2.status = 'critical'
|
||||
// CO2 calculations - only if co2 data is present in ANY sensor
|
||||
const co2Sensors = roomSensors.filter((sensor) => sensor.co2?.value !== undefined)
|
||||
if (co2Sensors.length > 0) {
|
||||
// Initialize co2 object if it doesn't exist
|
||||
if (!roomMetrics.co2) {
|
||||
roomMetrics.co2 = {
|
||||
current: 0,
|
||||
average: 0,
|
||||
max: 0,
|
||||
status: 'good',
|
||||
unit: data.co2?.unit || 'ppm',
|
||||
}
|
||||
}
|
||||
|
||||
// Occupancy estimate based on CO2 levels
|
||||
if (roomMetrics.co2.current < 600) roomMetrics.occupancyEstimate = 'low'
|
||||
else if (roomMetrics.co2.current < 1200) roomMetrics.occupancyEstimate = 'medium'
|
||||
else roomMetrics.occupancyEstimate = 'high'
|
||||
const co2Values = co2Sensors.map((sensor) => sensor.co2?.value || 0)
|
||||
roomMetrics.co2.current = co2Values.reduce((sum, val) => sum + val, 0) / co2Values.length
|
||||
roomMetrics.co2.max = Math.max(roomMetrics.co2.max, ...co2Values)
|
||||
roomMetrics.co2.average = (roomMetrics.co2.average + roomMetrics.co2.current) / 2
|
||||
if (data.co2?.unit) {
|
||||
roomMetrics.co2.unit = data.co2.unit
|
||||
}
|
||||
|
||||
// CO2 status classification
|
||||
if (roomMetrics.co2.current < 400) roomMetrics.co2.status = 'good'
|
||||
else if (roomMetrics.co2.current < 1000) roomMetrics.co2.status = 'moderate'
|
||||
else if (roomMetrics.co2.current < 5000) roomMetrics.co2.status = 'poor'
|
||||
else roomMetrics.co2.status = 'critical'
|
||||
|
||||
// Occupancy estimate based on CO2 levels
|
||||
if (roomMetrics.co2.current < 600) roomMetrics.occupancyEstimate = 'low'
|
||||
else if (roomMetrics.co2.current < 1200) roomMetrics.occupancyEstimate = 'medium'
|
||||
else roomMetrics.occupancyEstimate = 'high'
|
||||
}
|
||||
|
||||
roomMetrics.lastUpdated = data.timestamp
|
||||
}
|
||||
@@ -192,8 +241,8 @@ export const useRoomStore = defineStore('room', () => {
|
||||
sensorCount: sensorsInRoom.length,
|
||||
sensorTypes: [...new Set(sensorsInRoom.map((s) => s.type))],
|
||||
hasMetrics: !!roomMetrics,
|
||||
energyConsumption: roomMetrics?.energy.current || 0,
|
||||
co2Level: roomMetrics?.co2.current || 0,
|
||||
energyConsumption: roomMetrics?.energy?.current || 0,
|
||||
co2Level: roomMetrics?.co2?.current || 0,
|
||||
lastUpdated: roomMetrics?.lastUpdated || null,
|
||||
}
|
||||
}
|
||||
@@ -263,11 +312,11 @@ export const useRoomStore = defineStore('room', () => {
|
||||
const result = await handleApiCall(() => roomsApi.getRooms())
|
||||
if (result) {
|
||||
// Handle both response formats: {rooms: [...]} or direct array [...]
|
||||
const roomsArray = Array.isArray(result) ? result : result.rooms || []
|
||||
const roomsArray = Array.isArray(result) ? result : (result as any).rooms || []
|
||||
apiRooms.value = roomsArray
|
||||
|
||||
// Update available rooms from API data
|
||||
const roomNames = roomsArray.map((room) => room.name || room.room).filter((name) => name)
|
||||
const roomNames = roomsArray.map((room: any) => room.name || room.room).filter((name: string) => name)
|
||||
if (roomNames.length > 0) {
|
||||
availableRooms.value = [...new Set([...availableRooms.value, ...roomNames])].sort()
|
||||
}
|
||||
|
||||
@@ -124,22 +124,19 @@ export const useWebSocketStore = defineStore('websocket', () => {
|
||||
const sensorStore = useSensorStore()
|
||||
const roomStore = useRoomStore()
|
||||
|
||||
// Handle new multi-metric data
|
||||
// Only update room data if we have the proper structure
|
||||
if (data.energy && data.co2 && data.room) {
|
||||
// Update individual sensor readings first
|
||||
sensorStore.updateLatestReading(data)
|
||||
|
||||
// Update room data if we have room information (accepts partial readings)
|
||||
if (data.room) {
|
||||
if (data.energy) {
|
||||
sensorStore.updateEnergySensors(data)
|
||||
}
|
||||
roomStore.updateRoomData(data)
|
||||
}
|
||||
|
||||
// Map the sensor ID for individual sensor updates
|
||||
// const mappedSensorId = mapWebSocketSensorId(data.sensorId)
|
||||
const mappedData = { ...data, sensorId: data.sensorId, id: data.sensorId }
|
||||
sensorStore.updateLatestReading(data) // Update individual sensor readings for cards
|
||||
|
||||
// Update time series for chart if energy data is available
|
||||
if (data.energy) {
|
||||
// Update time series for chart (use energy values if available)
|
||||
const newLabel = new Date(data.timestamp * 1000).toLocaleTimeString()
|
||||
timeSeriesData.labels.push(newLabel)
|
||||
timeSeriesData.datasets[0].data.push(data.energy?.value)
|
||||
|
||||
Reference in New Issue
Block a user