Refactor sensor ID usage and types, add CO2 metrics, update docs

- Standardize on `sensor.sensor_id` throughout components and stores -
Add average and max CO2 metrics to sensor store and HomeView - Improve
type safety for sensors, actions, and API calls - Update AGENTS.md with
repository guidelines - Refine settings store types and utility
functions - Add WindowWithAuth interface for auth store access - Minor
bug fixes and code cleanup
This commit is contained in:
rafaeldpsilva
2025-10-01 14:04:25 +01:00
parent a518665673
commit f96456ed29
12 changed files with 189 additions and 76 deletions

View File

@@ -9,7 +9,7 @@
</div>
<div>
<h3 class="font-medium text-gray-900">{{ sensor.name }}</h3>
<p class="text-sm text-gray-500">{{ sensor.id }}</p>
<p class="text-sm text-gray-500">{{ sensor.sensor_id }}</p>
</div>
</div>
<div class="flex items-center gap-2">
@@ -30,9 +30,9 @@
<!-- Room Assignment -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Room Assignment</label>
<select
:value="sensor.room"
@change="$emit('updateRoom', sensor.id, ($event.target as HTMLSelectElement).value)"
<select
:value="sensor.room"
@change="$emit('updateRoom', sensor.sensor_id, ($event.target as HTMLSelectElement).value)"
class="w-full px-3 py-2 border border-gray-200 rounded-lg bg-white text-sm"
>
<option value="">Unassigned</option>
@@ -100,7 +100,7 @@
<span class="font-medium">Location:</span>
<div>{{ sensor.metadata.location }}</div>
</div>
<div>
<div v-if="sensor.lastSeen">
<span class="font-medium">Last Seen:</span>
<div>{{ formatTime(sensor.lastSeen) }}</div>
</div>
@@ -166,26 +166,27 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useSensorStore } from '@/stores/sensor'
import type { SensorDevice, SensorAction } from '@/services'
const props = defineProps<{
sensor: any
sensor: SensorDevice
availableRooms: string[]
isExecutingAction?: boolean
}>()
const emit = defineEmits<{
updateRoom: [sensorId: string, newRoom: string]
executeAction: [sensor: any, action: any]
executeAction: [sensor: SensorDevice, action: SensorAction]
}>()
const sensorStore = useSensorStore()
const getSensorValues = (sensor: any) => {
const getSensorValues = (sensor: SensorDevice) => {
const values = []
// Get real-time sensor reading from store
const latestReading = sensorStore.latestReadings.get(sensor.id) || sensorStore.latestReadings.get(sensor.sensor_id)
console.log(`[Detailed] Getting values for sensor ${sensor.id}, found reading:`, latestReading)
const latestReading = sensorStore.latestReadings.get(sensor.sensor_id)
console.log(`[Detailed] Getting values for sensor ${sensor.sensor_id}, found reading:`, latestReading)
console.log('[Detailed] Available readings:', Array.from(sensorStore.latestReadings.keys()))
console.log(`[Detailed] Sensor capabilities:`, sensor.capabilities?.monitoring)
@@ -315,25 +316,24 @@ const sensorValues = computed(() => getSensorValues(props.sensor))
// Check if sensor was recently updated for pulsing animation
const isRecentlyUpdated = computed(() => {
return sensorStore.recentlyUpdatedSensors.has(props.sensor.id) ||
sensorStore.recentlyUpdatedSensors.has(props.sensor.sensor_id)
return sensorStore.recentlyUpdatedSensors.has(props.sensor.sensor_id)
})
const getDefaultTags = (sensor: any) => {
const tags = [sensor.type]
if (sensor.metadata.battery) {
const getDefaultTags = (sensor: SensorDevice): string[] => {
const tags: string[] = [sensor.type]
if (sensor.metadata?.battery) {
tags.push('wireless')
} else {
tags.push('wired')
}
if (sensor.capabilities.actions.length > 0) {
tags.push('controllable')
} else {
tags.push('monitor-only')
}
return tags
}

View File

@@ -131,14 +131,20 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import type { SensorDevice, SensorAction } from '@/services'
interface ActionParameters {
value?: number | string | boolean
[key: string]: unknown
}
const props = defineProps<{
sensor: any
action: any
sensor: SensorDevice
action: SensorAction
}>()
const emit = defineEmits<{
execute: [sensorId: string, actionId: string, parameters: any]
execute: [sensorId: string, actionId: string, parameters: ActionParameters]
close: []
}>()
@@ -182,7 +188,7 @@ const getUnit = () => {
const executeAction = async () => {
isExecuting.value = true
const parameters: any = {}
const parameters: ActionParameters = {}
if (props.action.type === 'adjust') {
if (hasNumericRange.value) {
@@ -195,7 +201,7 @@ const executeAction = async () => {
}
try {
emit('execute', props.sensor.id, props.action.id, parameters)
emit('execute', props.sensor.sensor_id, props.action.id, parameters)
} catch (error) {
console.error('Failed to execute action:', error)
} finally {

View File

@@ -3,12 +3,15 @@
* Provides reactive API state management
*/
import { ref, reactive } from 'vue'
import {
sensorsApi,
roomsApi,
analyticsApi,
import {
sensorsApi,
roomsApi,
analyticsApi,
healthApi,
type SensorInfo,
type SensorDevice,
type SensorType,
type SensorStatus,
type SensorMetadata,
type RoomInfo,
type RoomData,
type AnalyticsSummary,
@@ -69,23 +72,23 @@ export function useSensorsApi() {
error: null
})
const sensors = ref<SensorInfo[]>([])
const currentSensor = ref<SensorInfo | null>(null)
const sensors = ref<SensorDevice[]>([])
const currentSensor = ref<SensorDevice | null>(null)
const sensorData = ref<DataResponse | null>(null)
const { handleApiCall } = useApi()
const fetchSensors = async (params?: {
room?: string
sensor_type?: any
status?: any
sensor_type?: SensorType
status?: SensorStatus
}) => {
const result = await handleApiCall(
() => sensorsApi.getSensors(params),
state
)
if (result) {
sensors.value = result
if (result && result.sensors) {
sensors.value = result.sensors
}
return result
}
@@ -129,7 +132,7 @@ export function useSensorsApi() {
const updateSensorMetadata = async (
sensorId: string,
metadata: Record<string, any>
metadata: SensorMetadata
) => {
return handleApiCall(
() => sensorsApi.updateSensorMetadata(sensorId, metadata),

View File

@@ -1,11 +1,19 @@
// Base configuration
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'
// Extend Window interface for auth store
interface WindowWithAuth extends Window {
__AUTH_STORE__?: {
getAuthHeader: () => Record<string, string>
ensureAuthenticated: () => Promise<boolean>
}
}
// API Response types
export interface ApiResponse<T = any> {
export interface ApiResponse<T = unknown> {
data: T
total_count?: number
query?: any
query?: Record<string, unknown>
execution_time_ms?: number
}
@@ -71,7 +79,7 @@ export interface SensorReading {
value: number
unit: string
}
metadata?: Record<string, any>
metadata?: Record<string, unknown>
}
export interface RoomInfo {
@@ -169,7 +177,7 @@ export interface SystemEvent {
event_type: string
severity: 'info' | 'warning' | 'error' | 'critical'
message: string
details?: Record<string, any>
details?: Record<string, unknown>
sensor_id?: string
room?: string
}
@@ -187,6 +195,9 @@ export interface SensorDevice {
actions: SensorAction[]
}
metadata: SensorMetadata
tags?: string[]
lastSeen?: number
total_readings?: number
}
export interface SensorAction {
@@ -231,6 +242,7 @@ export enum SensorStatus {
ONLINE = 'online',
OFFLINE = 'offline',
ERROR = 'error',
ACTIVE = 'active',
}
export interface SensorMetadata {
@@ -242,6 +254,7 @@ export interface SensorMetadata {
model?: string
firmware?: string
battery?: number
signalStrength?: number
created_at?: string
updated_at?: string
manufacturer?: string
@@ -277,7 +290,7 @@ class ApiClient {
// Dynamically get auth headers to avoid circular imports
try {
// Try to get from window first (for when store is exposed)
const authStore = (window as any).__AUTH_STORE__
const authStore = (window as WindowWithAuth).__AUTH_STORE__
if (authStore && typeof authStore.getAuthHeader === 'function') {
return authStore.getAuthHeader()
}
@@ -333,7 +346,7 @@ class ApiClient {
}
}
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
async get<T>(endpoint: string, params?: Record<string, string | number | boolean | string[]>): Promise<T> {
const url = new URL(`${this.baseUrl}${endpoint}`)
if (params) {
@@ -366,14 +379,14 @@ class ApiClient {
return await response.json()
}
async post<T>(endpoint: string, data?: any): Promise<T> {
async post<T>(endpoint: string, data?: Record<string, unknown> | unknown[]): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
})
}
async put<T>(endpoint: string, data?: any): Promise<T> {
async put<T>(endpoint: string, data?: Record<string, unknown> | unknown[]): Promise<T> {
return this.request<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,

View File

@@ -77,12 +77,22 @@ export const sensorsApi = {
}): Promise<{
data: SensorReading[]
count: number
export_params: any
export_params: {
start_time: number
end_time: number
sensor_ids?: string
format?: 'json' | 'csv'
}
}> {
return apiClient.get<{
data: SensorReading[]
count: number
export_params: any
export_params: {
start_time: number
end_time: number
sensor_ids?: string
format?: 'json' | 'csv'
}
}>('/api/v1/export', params)
},
}

View File

@@ -10,6 +10,13 @@ import {
type HealthCheck,
} from '@/services'
// Extend Window interface for auth store
interface WindowWithAuth extends Window {
__AUTH_STORE__?: {
ensureAuthenticated: () => Promise<boolean>
}
}
export const useAnalyticsStore = defineStore('analytics', () => {
// State
const analyticsData = ref<{
@@ -41,7 +48,7 @@ export const useAnalyticsStore = defineStore('analytics', () => {
console.warn('Authentication error detected, attempting to re-authenticate...')
try {
const authStore = (window as any).__AUTH_STORE__
const authStore = (window as WindowWithAuth).__AUTH_STORE__
if (authStore && typeof authStore.ensureAuthenticated === 'function') {
const authSuccess = await authStore.ensureAuthenticated()
if (authSuccess) {

View File

@@ -3,6 +3,13 @@ import { ref, reactive } from 'vue'
import { roomsApi, type RoomInfo as ApiRoomInfo, type SensorReading } from '@/services'
import { useSensorStore } from './sensor'
// Extend Window interface for auth store
interface WindowWithAuth extends Window {
__AUTH_STORE__?: {
ensureAuthenticated: () => Promise<boolean>
}
}
interface RoomMetrics {
room: string
sensors: string[]
@@ -221,7 +228,7 @@ export const useRoomStore = defineStore('room', () => {
console.warn('Authentication error detected, attempting to re-authenticate...')
try {
const authStore = (window as any).__AUTH_STORE__
const authStore = (window as WindowWithAuth).__AUTH_STORE__
if (authStore && typeof authStore.ensureAuthenticated === 'function') {
const authSuccess = await authStore.ensureAuthenticated()
if (authSuccess) {

View File

@@ -6,13 +6,21 @@ import {
type SensorDevice,
type SensorStatus,
type SensorReading,
type SensorMetadata,
} from '@/services'
// Extend Window interface for auth store
interface WindowWithAuth extends Window {
__AUTH_STORE__?: {
ensureAuthenticated: () => Promise<boolean>
}
}
export const useSensorStore = defineStore('sensor', () => {
// State
const sensorDevices = reactive<Map<string, SensorDevice>>(new Map())
const latestReadings = reactive<Map<string, SensorReading>>(new Map())
const sensorsData = reactive<Map<string, any>>(new Map()) // Legacy support
const sensorsData = reactive<Map<string, SensorReading>>(new Map()) // Legacy support - deprecated, use latestReadings instead
const recentlyUpdatedSensors = reactive<Set<string>>(new Set()) // Track recently updated sensors
const totalReadings = ref<number>(0) // Total number of readings across all sensors
const apiLoading = ref<boolean>(false)
@@ -27,6 +35,26 @@ export const useSensorStore = defineStore('sensor', () => {
).length
})
// Aggregated CO2 metrics
const averageCO2Level = computed<number>(() => {
const readings = Array.from(latestReadings.values())
const co2Readings = readings.filter(r => r.co2?.value !== undefined)
if (co2Readings.length === 0) return 0
const totalCO2 = co2Readings.reduce((sum, r) => sum + (r.co2?.value || 0), 0)
return totalCO2 / co2Readings.length
})
const maxCO2Level = computed<number>(() => {
const readings = Array.from(latestReadings.values())
const co2Values = readings
.filter(r => r.co2?.value !== undefined)
.map(r => r.co2?.value || 0)
return co2Values.length > 0 ? Math.max(...co2Values) : 0
})
// Actions
function updateSensorRoom(sensorId: string, newRoom: string): void {
const sensor = sensorDevices.get(sensorId)
@@ -93,7 +121,7 @@ export const useSensorStore = defineStore('sensor', () => {
console.warn('Authentication error detected, attempting to re-authenticate...')
try {
const authStore = (window as any).__AUTH_STORE__
const authStore = (window as WindowWithAuth).__AUTH_STORE__
if (authStore && typeof authStore.ensureAuthenticated === 'function') {
const authSuccess = await authStore.ensureAuthenticated()
if (authSuccess) {
@@ -193,7 +221,7 @@ export const useSensorStore = defineStore('sensor', () => {
let totalReadingsCount: number = 0
result.sensors.forEach((sensor) => {
const sensorKey: string = sensor._id || sensor.sensor_id
const sensorKey: string = sensor.sensor_id
const sensorType: string = sensor.sensor_type || sensor.type
const sensorName: string = sensor.name || ''
@@ -204,14 +232,12 @@ export const useSensorStore = defineStore('sensor', () => {
const normalizedSensor = {
...sensor,
id: sensorKey,
type: sensorType,
capabilities: {
actions: [], // Default empty actions array
monitoring:
sensor.capabilities?.monitoring ||
getDefaultMonitoringCapabilities(sensorType, sensorName),
...sensor.capabilities,
actions: sensor.capabilities?.actions || [],
},
metadata: {
model: sensor.metadata?.model || 'Unknown',
@@ -221,10 +247,10 @@ export const useSensorStore = defineStore('sensor', () => {
signalStrength: sensor.metadata?.signalStrength,
...sensor.metadata,
},
lastSeen: sensor.last_seen || Date.now() / 1000,
lastSeen: sensor.lastSeen || Date.now() / 1000,
}
sensorDevices.set(sensorKey, normalizedSensor)
sensorDevices.set(sensorKey, normalizedSensor as SensorDevice)
})
// Update total readings
@@ -246,7 +272,7 @@ export const useSensorStore = defineStore('sensor', () => {
return handleApiCall(() => sensorsApi.getSensorData(sensorId, params))
}
async function updateApiSensorMetadata(sensorId: string, metadata: Record<string, any>) {
async function updateApiSensorMetadata(sensorId: string, metadata: SensorMetadata) {
return handleApiCall(() => sensorsApi.updateSensorMetadata(sensorId, metadata))
}
@@ -276,6 +302,8 @@ export const useSensorStore = defineStore('sensor', () => {
// Computed
totalSensors,
activeSensors,
averageCO2Level,
maxCO2Level,
// Actions
updateEnergySensors,

View File

@@ -33,6 +33,18 @@ interface AppSettings {
developerMode: boolean
}
// Type for setting values
type SettingValue =
| string
| number
| boolean
| Theme
| Language
| NavigationMode
| UISettings
| NotificationSettings
| AppSettings
const DEFAULT_SETTINGS: AppSettings = {
theme: 'system',
language: 'en',
@@ -93,8 +105,9 @@ export const useSettingsStore = defineStore('settings', () => {
saveSettings()
}
function updateSetting(path: string, value: any) {
function updateSetting(path: string, value: SettingValue): void {
const keys = path.split('.')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let current: any = settings
for (let i = 0; i < keys.length - 1; i++) {
@@ -105,8 +118,9 @@ export const useSettingsStore = defineStore('settings', () => {
saveSettings()
}
function getSetting(path: string): any {
function getSetting(path: string): SettingValue | undefined {
const keys = path.split('.')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let current: any = settings
for (const key of keys) {
@@ -114,7 +128,7 @@ export const useSettingsStore = defineStore('settings', () => {
if (current === undefined) break
}
return current
return current as SettingValue | undefined
}
function exportSettings(): string {

View File

@@ -22,6 +22,8 @@
:content="websocketStore.isConnected ? 'Connected' : 'Disconnected'"
/>
<MetricCard title="Average Usage" :content="averageEnergyUsage" details="kWh" />
<MetricCard title="Average CO2" :content="averageCO2" details="ppm" />
<MetricCard title="Max CO2" :content="maxCO2" details="ppm" />
<GraphMetricCard
title="Real-time Energy"
:content="currentEnergyValue"
@@ -29,18 +31,6 @@
:trend-data="energyStore.energyHistory.slice(-8)"
trend-direction="neutral"
/>
<GraphMetricCard
title="Current Knowledge"
content="86%"
:trend-data="[203, 78, 80, 82, 142, 85, 85, 86]"
trend-direction="down"
/>
<GraphMetricCard
title="Knowledge Gain"
content="+34%"
:trend-data="[20, 25, 28, 30, 32, 33, 34, 34]"
trend-direction="neutral"
/>
</div>
<div>
<RealtimeEnergyChartCard title="Month" />
@@ -66,11 +56,13 @@ import SensorConsumptionTable from '@/components/cards/SensorConsumptionTable.vu
import RoomMetricsCard from '@/components/cards/RoomMetricsCard.vue'
import AirQualityCard from '@/components/cards/AirQualityCard.vue'
import { useEnergyStore } from '@/stores/energy'
import { useSensorStore } from '@/stores/sensor'
import { useSettingsStore } from '@/stores/settings'
import { computed, onMounted, onUnmounted } from 'vue'
import { useWebSocketStore } from '@/stores/websocket'
const energyStore = useEnergyStore()
const sensorStore = useSensorStore()
const websocketStore = useWebSocketStore()
const settingsStore = useSettingsStore()
@@ -83,6 +75,14 @@ const averageEnergyUsage = computed(() => {
return energyStore.averageEnergyUsage.toFixed(2)
})
const averageCO2 = computed(() => {
return Math.round(sensorStore.averageCO2Level)
})
const maxCO2 = computed(() => {
return Math.round(sensorStore.maxCO2Level)
})
onMounted(() => {
settingsStore.initialize()

View File

@@ -201,6 +201,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useSensorStore } from '@/stores/sensor'
import { useRoomStore } from '@/stores/room'
import { useWebSocketStore } from '@/stores/websocket'
import type { SensorDevice, SensorAction } from '@/services'
import ActionModal from '@/components/modals/ActionModal.vue'
import RoomManagementModal from '@/components/modals/RoomManagementModal.vue'
import SimpleSensorCard from '@/components/cards/SimpleSensorCard.vue'
@@ -217,8 +218,8 @@ const selectedType = ref('')
const selectedStatus = ref('')
const showActionModal = ref(false)
const selectedSensor = ref<any>(null)
const selectedAction = ref<any>(null)
const selectedSensor = ref<SensorDevice | null>(null)
const selectedAction = ref<SensorAction | null>(null)
const isExecutingAction = ref(false)
const showRoomManagementModal = ref(false)
@@ -241,17 +242,22 @@ const updateRoom = (sensorId: string, newRoom: string) => {
sensorStore.updateSensorRoom(sensorId, newRoom)
}
const executeAction = (sensor: any, action: any) => {
const executeAction = (sensor: SensorDevice, action: SensorAction) => {
if (action.parameters) {
selectedSensor.value = sensor
selectedAction.value = action
showActionModal.value = true
} else {
handleActionExecute(sensor.id, action.id, {})
handleActionExecute(sensor.sensor_id, action.id, {})
}
}
const handleActionExecute = async (sensorId: string, actionId: string, parameters: any) => {
interface ActionParameters {
value?: number | string | boolean
[key: string]: unknown
}
const handleActionExecute = async (sensorId: string, actionId: string, parameters: ActionParameters) => {
isExecutingAction.value = true
try {
await sensorStore.executeSensorAction(sensorId, actionId)