Refactor sensor data handling for new API and WebSocket format

- Update SensorConsumptionTable to use new sensorStore and
websocketStore - Normalize sensor and reading interfaces for consistency
- Remove legacy energy data handling and mapping logic - Update API and
store types for new backend schema - Fetch sensors on mount in
SensorConsumptionTable - Simplify WebSocket data processing and remove
legacy code
This commit is contained in:
rafaeldpsilva
2025-09-30 17:58:06 +01:00
parent 90b6034465
commit 83eaa7e121
6 changed files with 126 additions and 321 deletions

View File

@@ -29,21 +29,37 @@
No sensor data available. Waiting for WebSocket connection...
</td>
</tr>
<tr v-for="sensor in sensorList" :key="sensor.sensorId" class="hover:bg-gray-50">
<tr
v-for="sensor in sensorStore.latestReadings.values()"
:key="sensor.sensor_id"
class="hover:bg-gray-50"
>
<td class="py-3 text-sm font-medium text-gray-900">
{{ sensor.sensorId }}
{{ sensor.sensor_id }}
</td>
<td class="py-3 text-sm text-gray-600 text-right">
{{ sensor.latestValue.toFixed(2) }} {{ sensor.unit }}
{{ sensor.room }}
</td>
<td class="py-3 text-sm text-gray-600 text-right">
{{ sensor.totalConsumption.toFixed(2) }} {{ sensor.unit }}
{{
sensor.energy?.value ||
sensor.co2?.value ||
sensor.temperature?.value ||
sensor.humidity?.value ||
'N/A'
}}
{{
sensor.energy?.unit ||
sensor.co2?.unit ||
sensor.temperature?.unit ||
sensor.humidity?.unit
}}
</td>
<td class="py-3 text-sm text-gray-600 text-right">
{{ sensor.averageConsumption.toFixed(2) }} {{ sensor.unit }}
{{ sensor.room }}
</td>
<td class="py-3 text-sm text-gray-500 text-right">
{{ formatTime(sensor.lastUpdated) }}
{{ formatTime(sensor.timestamp) }}
</td>
</tr>
</tbody>
@@ -55,27 +71,25 @@
<div class="flex items-center gap-2">
<div
class="w-2 h-2 rounded-full"
:class="energyStore.isConnected ? 'bg-green-500' : 'bg-red-500'"
:class="websocketStore.isConnected ? 'bg-green-500' : 'bg-red-500'"
></div>
<span>{{ energyStore.isConnected ? 'Connected' : 'Disconnected' }}</span>
</div>
<div>
{{ sensorList.length }} sensor{{ sensorList.length !== 1 ? 's' : '' }} active
<span>{{ websocketStore.isConnected ? 'Connected' : 'Disconnected' }}</span>
</div>
<div>{{ sensorList.length }} sensor{{ sensorList.length !== 1 ? 's' : '' }} active</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useEnergyStore } from '@/stores/energy'
import { computed, onMounted } from 'vue'
import { useSensorStore } from '@/stores/sensor'
import { useWebSocketStore } from '@/stores/websocket'
const energyStore = useEnergyStore()
const sensorStore = useSensorStore()
const websocketStore = useWebSocketStore()
const sensorList = computed(() => {
return Array.from(energyStore.sensorsData.values()).sort((a, b) =>
a.sensorId.localeCompare(b.sensorId)
)
return Array.from(sensorStore.sensorDevices.values()).sort((a, b) => a.name.localeCompare(b.name))
})
const formatTime = (timestamp: number) => {
@@ -92,4 +106,8 @@ const formatTime = (timestamp: number) => {
return date.toLocaleTimeString()
}
}
onMounted(async () => {
await sensorStore.fetchApiSensors()
})
</script>

View File

@@ -50,12 +50,11 @@ export interface DataResponse {
}
export interface SensorReading {
_id?: string
sensor_id: string
room?: string
room: string
sensor_type: string
timestamp: number
created_at?: string
type: string
energy?: {
value: number
unit: string
@@ -176,29 +175,18 @@ export interface SystemEvent {
}
export interface SensorDevice {
id: string
_id: string
sensor_id: string
name: string
type: SensorType
sensor_type: SensorType
room: string
status: SensorStatus
location?: string
lastSeen: number
total_readings?: number
capabilities: {
monitoring: string[]
actions: SensorAction[]
}
metadata: {
location?: string
model?: string
firmware?: string
battery?: number
created_at?: string
updated_at?: string
manufacturer?: string
}
metadata: SensorMetadata
}
export interface SensorAction {
@@ -245,7 +233,20 @@ export enum SensorStatus {
ERROR = 'error',
}
// HTTP Client class
export interface SensorMetadata {
time_factor?: number
occupancy_factor?: number
quality_level?: string
duration_seconds?: number
location?: string
model?: string
firmware?: string
battery?: number
created_at?: string
updated_at?: string
manufacturer?: string
}
class ApiClient {
private baseUrl: string

View File

@@ -6,6 +6,7 @@ import {
type DataResponse,
type SensorType,
type SensorStatus,
type SensorMetadata,
} from './api'
export const sensorsApi = {
@@ -13,8 +14,24 @@ export const sensorsApi = {
room?: string
sensor_type?: SensorType
status?: SensorStatus
}): Promise<SensorDevice[]> {
return apiClient.get<SensorDevice[]>('/api/v1/sensors/get', params)
}): Promise<{
sensors: SensorDevice[]
count: number
filters: {
room: string
sensor_type: SensorType
status: SensorStatus
}
}> {
return apiClient.get<{
sensors: SensorDevice[]
count: number
filters: {
room: string
sensor_type: SensorType
status: SensorStatus
}
}>('/api/v1/sensors/get', params)
},
async getSensor(sensorId: string): Promise<SensorDevice> {
@@ -39,7 +56,7 @@ export const sensorsApi = {
async updateSensorMetadata(
sensorId: string,
metadata: Record<string, any>,
metadata: SensorMetadata,
): Promise<{ message: string }> {
return apiClient.put<{ message: string }>(`/api/v1/sensors/${sensorId}/metadata`, metadata)
},

View File

@@ -3,18 +3,12 @@ import { computed } from 'vue'
import { useSensorStore } from './sensor'
import { useRoomStore } from './room'
import { useAnalyticsStore } from './analytics'
import { useWebSocketStore } from './websocket'
export const useEnergyStore = defineStore('energy', () => {
// Get instances of other stores
const sensorStore = useSensorStore()
const roomStore = useRoomStore()
const analyticsStore = useAnalyticsStore()
const webSocketStore = useWebSocketStore()
// Delegate to WebSocket store
const connect = (url: string) => webSocketStore.connect(url)
const disconnect = () => webSocketStore.disconnect()
// Initialize data from APIs
async function initializeFromApi() {
@@ -27,15 +21,9 @@ export const useEnergyStore = defineStore('energy', () => {
}
return {
// WebSocket state (delegated)
isConnected: computed(() => webSocketStore.isConnected),
latestMessage: computed(() => webSocketStore.latestMessage),
timeSeriesData: computed(() => webSocketStore.timeSeriesData),
// Sensor state (delegated)
sensorsData: computed(() => sensorStore.sensorsData),
sensorDevices: computed(() => sensorStore.sensorDevices),
latestReadings: computed(() => sensorStore.latestReadings),
apiSensors: computed(() => Array.from(sensorStore.sensorDevices.values())), // Convert Map to Array
// Room state (delegated)
@@ -51,13 +39,11 @@ export const useEnergyStore = defineStore('energy', () => {
healthStatus: computed(() => analyticsStore.healthStatus),
// Combined API loading/error state
apiLoading: computed(() => sensorStore.apiLoading || roomStore.apiLoading || analyticsStore.apiLoading),
apiLoading: computed(
() => sensorStore.apiLoading || roomStore.apiLoading || analyticsStore.apiLoading,
),
apiError: computed(() => sensorStore.apiError || roomStore.apiError || analyticsStore.apiError),
// WebSocket functions (delegated)
connect,
disconnect,
// Sensor functions (delegated)
updateSensorRoom: sensorStore.updateSensorRoom,
executeSensorAction: sensorStore.executeSensorAction,

View File

@@ -3,37 +3,11 @@ import { ref, reactive } from 'vue'
import {
sensorsApi,
SensorType,
SensorStatus,
type SensorDevice,
type SensorAction,
type SensorStatus,
type SensorReading,
} from '@/services'
interface SensorReading {
id: string
sensorId: string
room: string
timestamp: number
energy: {
value: number
unit: string
}
co2: {
value: number
unit: string
}
temperature?: {
value: number
unit: string
}
}
interface LegacyEnergyData {
sensorId: string
timestamp: number
value: number
unit: string
}
export const useSensorStore = defineStore('sensor', () => {
// State
const sensorDevices = reactive<Map<string, SensorDevice>>(new Map())
@@ -52,7 +26,7 @@ export const useSensorStore = defineStore('sensor', () => {
}
}
async function executeSensorAction(sensorId: string, actionId: string, parameters?: any) {
async function executeSensorAction(sensorId: string, actionId: string) {
const sensor = sensorDevices.get(sensorId)
if (!sensor) return false
@@ -75,49 +49,18 @@ export const useSensorStore = defineStore('sensor', () => {
return Array.from(sensorDevices.values()).filter((sensor) => sensor.type === type)
}
function updateSensorData(data: LegacyEnergyData) {
const existingSensor = sensorsData.get(data.sensorId)
if (existingSensor) {
const newTotal = existingSensor.totalConsumption + data.value
const dataPoints = Math.floor((data.timestamp - existingSensor.lastUpdated) / 60) + 1
sensorsData.set(data.sensorId, {
...existingSensor,
latestValue: data.value,
totalConsumption: newTotal,
averageConsumption: newTotal / dataPoints,
lastUpdated: data.timestamp,
})
} else {
sensorsData.set(data.sensorId, {
sensorId: data.sensorId,
latestValue: data.value,
totalConsumption: data.value,
averageConsumption: data.value,
lastUpdated: data.timestamp,
unit: data.unit,
})
}
// Mark sensor as recently updated for legacy data as well
recentlyUpdatedSensors.add(data.sensorId)
// Remove from recently updated after 2 seconds
setTimeout(() => {
recentlyUpdatedSensors.delete(data.sensorId)
}, 2000)
function updateEnergySensors(data: Sensor) {
console.log(data)
}
function updateLatestReading(reading: SensorReading) {
latestReadings.set(reading.sensorId, reading)
latestReadings.set(reading.sensor_id, reading)
// Mark sensor as recently updated
recentlyUpdatedSensors.add(reading.sensorId)
console.log(reading.sensor_type)
recentlyUpdatedSensors.add(reading.sensor_id)
// Remove from recently updated after 2 seconds
setTimeout(() => {
recentlyUpdatedSensors.delete(reading.sensorId)
recentlyUpdatedSensors.delete(reading.sensor_id)
}, 2000)
}
@@ -225,17 +168,21 @@ export const useSensorStore = defineStore('sensor', () => {
}
// Sensors API functions
async function fetchApiSensors(params?: { room?: string; sensor_type?: any; status?: any }) {
async function fetchApiSensors(params?: {
room?: string
sensor_type?: SensorType
status?: SensorStatus
}) {
const result = await handleApiCall(() => sensorsApi.getSensors(params))
if (result) {
console.log(result)
// Check if result has a sensors property (common API pattern)
if (result.sensors && Array.isArray(result.sensors)) {
result.sensors.forEach((sensor) => {
const sensorKey = sensor.id || sensor._id || sensor.sensor_id
const sensorKey = sensor._id || sensor.sensor_id
const sensorType = sensor.sensor_type || sensor.type
const sensorName = sensor.name || ''
// Normalize sensor data structure for frontend compatibility
const normalizedSensor = {
...sensor,
id: sensorKey,
@@ -255,58 +202,18 @@ export const useSensorStore = defineStore('sensor', () => {
signalStrength: sensor.metadata?.signalStrength,
...sensor.metadata,
},
tags: sensor.tags || [],
lastSeen: sensor.last_seen || sensor.lastSeen || Date.now() / 1000,
lastSeen: sensor.last_seen || Date.now() / 1000,
}
sensorDevices.set(sensorKey, normalizedSensor)
})
}
// Check if result is directly an array
else if (Array.isArray(result)) {
console.log('Result is direct array:', result)
result.forEach((sensor) => {
console.log('Adding sensor:', sensor)
const sensorKey = sensor.id || sensor._id || sensor.sensor_id
const sensorType = sensor.sensor_type || sensor.type
const sensorName = sensor.name || ''
// Normalize sensor data structure for frontend compatibility
const normalizedSensor = {
...sensor,
id: sensorKey,
type: sensorType,
capabilities: {
actions: [], // Default empty actions array
monitoring:
sensor.capabilities?.monitoring ||
getDefaultMonitoringCapabilities(sensorType, sensorName),
...sensor.capabilities,
},
metadata: {
model: sensor.metadata?.model || 'Unknown',
firmware: sensor.metadata?.firmware || 'Unknown',
location: sensor.metadata?.location || sensor.room || 'Unknown',
battery: sensor.metadata?.battery,
signalStrength: sensor.metadata?.signalStrength,
...sensor.metadata,
},
tags: sensor.tags || [],
lastSeen: sensor.last_seen || sensor.lastSeen || Date.now() / 1000,
}
sensorDevices.set(sensorKey, normalizedSensor)
})
}
// Log what we actually got
else {
console.log('Unexpected result format:', typeof result, result)
} else {
console.warn('Unexpected result format:', typeof result, result)
}
} else {
console.log('No result received from API')
console.error('No result received from API')
}
console.log('Current sensor devices:', Array.from(sensorDevices.entries()))
return result
}
@@ -344,11 +251,11 @@ export const useSensorStore = defineStore('sensor', () => {
apiError,
// Actions
updateEnergySensors,
updateSensorRoom,
executeSensorAction,
getSensorsByRoom,
getSensorsByType,
updateSensorData,
updateLatestReading,
// API functions

View File

@@ -5,13 +5,6 @@ import { useRoomStore } from './room'
const MAX_DATA_POINTS = 100
interface LegacyEnergyData {
sensorId: string
timestamp: number
value: number
unit: string
}
interface SensorReading {
sensorId: string
room: string
@@ -31,9 +24,8 @@ interface SensorReading {
}
export const useWebSocketStore = defineStore('websocket', () => {
// State
const isConnected = ref(false)
const latestMessage = ref<LegacyEnergyData | null>(null)
const latestMessage = ref<SensorReading | null>(null)
const timeSeriesData = reactive<{
labels: string[]
datasets: { data: number[] }[]
@@ -43,16 +35,14 @@ export const useWebSocketStore = defineStore('websocket', () => {
})
let socket: WebSocket | null = null
const newDataBuffer: (LegacyEnergyData | SensorReading)[] = []
const newDataBuffer: SensorReading[] = []
// Actions
function connect(url: string) {
if (isConnected.value && socket) {
console.log('Already connected.')
return
}
// Close any existing connection first
if (socket) {
socket.onclose = null
socket.onerror = null
@@ -61,11 +51,11 @@ export const useWebSocketStore = defineStore('websocket', () => {
socket = null
}
console.log(`Connecting to WebSocket at ${url}`)
console.info(`Connecting to WebSocket at ${url}`)
socket = new WebSocket(url)
socket.onopen = () => {
console.log('WebSocket connection established.')
console.info('WebSocket connection established.')
isConnected.value = true
}
@@ -74,7 +64,7 @@ export const useWebSocketStore = defineStore('websocket', () => {
const data = JSON.parse(event.data)
if (data.type === 'proxy_info' && data.sensor_service_url) {
console.log('Received proxy info, reconnecting to sensor service...')
console.warn('Received proxy info, reconnecting to sensor service...')
if (socket) {
socket.onclose = null
socket.close()
@@ -83,7 +73,7 @@ export const useWebSocketStore = defineStore('websocket', () => {
isConnected.value = false
setTimeout(() => {
console.log('Connecting directly to sensor service at ws://localhost:8007/ws')
console.info('Connecting directly to sensor service at ws://localhost:8007/ws')
connect('ws://localhost:8007/ws')
}, 100)
return
@@ -125,155 +115,41 @@ export const useWebSocketStore = defineStore('websocket', () => {
}
}
function isLegacyData(data: any): data is LegacyEnergyData {
return 'value' in data && !('energy' in data) && !('co2' in data)
}
function mapWebSocketSensorIdd(webSocketSensorId: string): string {
const sensorStore = useSensorStore()
// First try exact match
if (sensorStore.sensorDevices.has(webSocketSensorId)) {
return webSocketSensorId
}
console.log(`Attempting to map WebSocket sensor ID: ${webSocketSensorId}`)
console.log(
'Available API sensors:',
Array.from(sensorStore.sensorDevices.entries()).map(([id, sensor]) => ({
id,
name: sensor.name,
room: sensor.room,
})),
)
// Try to find a sensor by matching room and type patterns
const sensors = Array.from(sensorStore.sensorDevices.entries())
// Pattern matching for common WebSocket ID formats
for (const [apiSensorId, sensor] of sensors) {
const sensorName = sensor.name.toLowerCase()
const sensorRoom = sensor.room?.toLowerCase() || ''
const wsId = webSocketSensorId.toLowerCase()
// Room-based matching (more comprehensive)
if (wsId.includes('living') && sensorName.includes('living')) return apiSensorId
if (wsId.includes('lr') && sensorName.includes('living')) return apiSensorId
if (wsId.includes('bt') && sensorName.includes('bathroom')) return apiSensorId // bt = bathroom
if (wsId.includes('bathroom') && sensorName.includes('bathroom')) return apiSensorId
if (wsId.includes('br') && sensorName.includes('bedroom')) return apiSensorId // br = bedroom
if (wsId.includes('bedroom') && sensorName.includes('bedroom')) return apiSensorId
if (wsId.includes('kt') && sensorName.includes('kitchen')) return apiSensorId // kt = kitchen
if (wsId.includes('kitchen') && sensorName.includes('kitchen')) return apiSensorId
if (wsId.includes('gr') && sensorName.includes('garage')) return apiSensorId // gr = garage
if (wsId.includes('garage') && sensorName.includes('garage')) return apiSensorId
// Type-based matching
if (wsId.includes('energy') && sensorName.includes('energy')) return apiSensorId
if (wsId.includes('co2') && sensorName.includes('co2')) return apiSensorId
if (wsId.includes('temp') && sensorName.includes('temp')) return apiSensorId
if (wsId.includes('humidity') && sensorName.includes('humidity')) return apiSensorId
// Combined room + type matching for better accuracy
if (
wsId.includes('bathroom') &&
wsId.includes('humidity') &&
sensorName.includes('bathroom') &&
sensorName.includes('humidity')
)
return apiSensorId
if (
wsId.includes('bedroom') &&
wsId.includes('temp') &&
sensorName.includes('bedroom') &&
sensorName.includes('temp')
)
return apiSensorId
if (
wsId.includes('kitchen') &&
wsId.includes('humidity') &&
sensorName.includes('kitchen') &&
sensorName.includes('humidity')
)
return apiSensorId
}
// If no mapping found, let's try to use the first sensor as a fallback for testing
if (sensors.length > 0) {
const fallbackSensor = sensors[0]
console.warn(
`No mapping found for ${webSocketSensorId}, using fallback sensor: ${fallbackSensor[1].name}`,
)
return fallbackSensor[0]
}
console.warn(`Could not map WebSocket sensor ID ${webSocketSensorId} to any API sensor`)
return webSocketSensorId // Return original if no mapping found
}
function processIncomingData(data: LegacyEnergyData | SensorReading) {
function processIncomingData(data: SensorReading) {
// Skip non-data messages
if ('type' in data && (data.type === 'connection_established' || data.type === 'proxy_info')) {
return
}
// Normalize property names: sensor_id -> sensorId
if ('sensor_id' in data && !('sensorId' in data)) {
data.sensorId = data.sensor_id
}
const sensorStore = useSensorStore()
const roomStore = useRoomStore()
// Handle both legacy and new data formats
if (isLegacyData(data)) {
latestMessage.value = data
sensorStore.updateSensorData(data)
// Convert legacy data to SensorReading format for individual sensor updates
const mappedSensorId = mapWebSocketSensorId(data.sensorId)
const sensorReading = {
sensorId: mappedSensorId,
room: 'Unknown', // Legacy data doesn't include room info
timestamp: data.timestamp,
energy: {
value: data.value,
unit: data.unit,
},
co2: {
value: 400, // Default CO2 value for legacy data
unit: 'ppm',
},
// Handle new multi-metric data
// Only update room data if we have the proper structure
if (data.energy && data.co2 && data.room) {
if (data.energy) {
sensorStore.updateEnergySensors(data)
}
sensorStore.updateLatestReading(sensorReading)
roomStore.updateRoomData(data)
}
// Update time series for chart
const newLabel = new Date(data.timestamp * 1000).toLocaleTimeString()
timeSeriesData.labels.push(newLabel)
timeSeriesData.datasets[0].data.push(data.value)
} else {
// Handle new multi-metric data
// Only update room data if we have the proper structure
if (data.energy && data.co2 && data.room) {
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(mappedData) // Update individual sensor readings for cards
// 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
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 || 0)
timeSeriesData.datasets[0].data.push(data.energy?.value)
}
}
// Keep only the latest data points
if (timeSeriesData.labels.length > MAX_DATA_POINTS) {
timeSeriesData.labels.shift()
timeSeriesData.datasets[0].data.shift()
}
// Keep only the latest data points
if (timeSeriesData.labels.length > MAX_DATA_POINTS) {
timeSeriesData.labels.shift()
timeSeriesData.datasets[0].data.shift()
}
return {