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:
19
AGENTS.md
Normal file
19
AGENTS.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
The Vue 3 frontend lives under `src/`. `main.ts` wires the router and Pinia, while `App.vue` hosts global layout. Page-level views sit in `src/views/` (e.g. `AnalyticsView.vue`), with shared widgets under `src/components/`. Pinia logic is grouped in `src/stores/` (one file per domain such as `energy.ts` or `room.ts`), and API/WebSocket helpers in `src/services/`. Reusable hooks belong in `src/composables/`. Static files and icons stay in `public/` or `src/assets/`. Keep demo tooling like `test-websocket.html` at the repo root; production builds land in `dist/`.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
Run `npm install` once after cloning. `npm run dev` starts Vite locally; use `npm run dev-server` when you need LAN access. `npm run build` performs a type-safe production build (calls `npm run type-check` plus `vite build`). `npm run preview` serves the built bundle. Execute `npm run test:unit` for Vitest suites, `npm run lint` for ESLint (auto-fix enabled), and `npm run format` to apply Prettier to `src/`.
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
Follow the ESLint + Prettier flat config: 2-space indentation, single quotes in TS, and script setup in Vue SFCs when practical. Name Vue files in PascalCase (`EnergyOverviewCard.vue`), stores in camelCase (`energy.ts` exporting `useEnergyStore`), and composables with the `use` prefix. Keep Tailwind utility classes readable by grouping per concern. Avoid unchecked `console.log`; prefer the logging helpers already present in stores.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
Vitest with the `jsdom` environment powers unit tests; place suites alongside features in `src/**/__tests__/` using `*.spec.ts`. Mock API and WebSocket calls by leveraging Pinia store injection or `vi.mock('../services/api')`. Every new store action or view-level computed branch should gain coverage. Run `npm run test:unit -- --run --coverage` before opening a PR if you add complex domain logic.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
Commits use short, imperative summaries without prefixes (see `git log`). Keep subject lines under ~70 characters and include the affected area, e.g., `Refine room status badges`. Squash fixups locally rather than pushing noisy history. PRs should link to Jira/GitHub issues when relevant, include screenshots or GIFs for UI changes, list test commands run, and call out backend dependencies (e.g., new API fields).
|
||||||
|
|
||||||
|
## Environment & Configuration
|
||||||
|
Frontend defaults to `http://localhost:8000`; override with `VITE_API_BASE_URL` in a `.env.local`. Document new environment flags in `README.md`. Never commit real credentials—use the provided TypeScript definitions in `env.d.ts` to keep variable access typed.
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-medium text-gray-900">{{ sensor.name }}</h3>
|
<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>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Room Assignment</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">Room Assignment</label>
|
||||||
<select
|
<select
|
||||||
:value="sensor.room"
|
:value="sensor.room"
|
||||||
@change="$emit('updateRoom', sensor.id, ($event.target as HTMLSelectElement).value)"
|
@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"
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg bg-white text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Unassigned</option>
|
<option value="">Unassigned</option>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
<span class="font-medium">Location:</span>
|
<span class="font-medium">Location:</span>
|
||||||
<div>{{ sensor.metadata.location }}</div>
|
<div>{{ sensor.metadata.location }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="sensor.lastSeen">
|
||||||
<span class="font-medium">Last Seen:</span>
|
<span class="font-medium">Last Seen:</span>
|
||||||
<div>{{ formatTime(sensor.lastSeen) }}</div>
|
<div>{{ formatTime(sensor.lastSeen) }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,26 +166,27 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useSensorStore } from '@/stores/sensor'
|
import { useSensorStore } from '@/stores/sensor'
|
||||||
|
import type { SensorDevice, SensorAction } from '@/services'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
sensor: any
|
sensor: SensorDevice
|
||||||
availableRooms: string[]
|
availableRooms: string[]
|
||||||
isExecutingAction?: boolean
|
isExecutingAction?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
updateRoom: [sensorId: string, newRoom: string]
|
updateRoom: [sensorId: string, newRoom: string]
|
||||||
executeAction: [sensor: any, action: any]
|
executeAction: [sensor: SensorDevice, action: SensorAction]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const sensorStore = useSensorStore()
|
const sensorStore = useSensorStore()
|
||||||
|
|
||||||
const getSensorValues = (sensor: any) => {
|
const getSensorValues = (sensor: SensorDevice) => {
|
||||||
const values = []
|
const values = []
|
||||||
|
|
||||||
// Get real-time sensor reading from store
|
// Get real-time sensor reading from store
|
||||||
const latestReading = sensorStore.latestReadings.get(sensor.id) || sensorStore.latestReadings.get(sensor.sensor_id)
|
const latestReading = sensorStore.latestReadings.get(sensor.sensor_id)
|
||||||
console.log(`[Detailed] Getting values for sensor ${sensor.id}, found reading:`, latestReading)
|
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] Available readings:', Array.from(sensorStore.latestReadings.keys()))
|
||||||
console.log(`[Detailed] Sensor capabilities:`, sensor.capabilities?.monitoring)
|
console.log(`[Detailed] Sensor capabilities:`, sensor.capabilities?.monitoring)
|
||||||
|
|
||||||
@@ -315,14 +316,13 @@ const sensorValues = computed(() => getSensorValues(props.sensor))
|
|||||||
|
|
||||||
// Check if sensor was recently updated for pulsing animation
|
// Check if sensor was recently updated for pulsing animation
|
||||||
const isRecentlyUpdated = computed(() => {
|
const isRecentlyUpdated = computed(() => {
|
||||||
return sensorStore.recentlyUpdatedSensors.has(props.sensor.id) ||
|
return sensorStore.recentlyUpdatedSensors.has(props.sensor.sensor_id)
|
||||||
sensorStore.recentlyUpdatedSensors.has(props.sensor.sensor_id)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const getDefaultTags = (sensor: any) => {
|
const getDefaultTags = (sensor: SensorDevice): string[] => {
|
||||||
const tags = [sensor.type]
|
const tags: string[] = [sensor.type]
|
||||||
|
|
||||||
if (sensor.metadata.battery) {
|
if (sensor.metadata?.battery) {
|
||||||
tags.push('wireless')
|
tags.push('wireless')
|
||||||
} else {
|
} else {
|
||||||
tags.push('wired')
|
tags.push('wired')
|
||||||
|
|||||||
@@ -131,14 +131,20 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import type { SensorDevice, SensorAction } from '@/services'
|
||||||
|
|
||||||
|
interface ActionParameters {
|
||||||
|
value?: number | string | boolean
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
sensor: any
|
sensor: SensorDevice
|
||||||
action: any
|
action: SensorAction
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
execute: [sensorId: string, actionId: string, parameters: any]
|
execute: [sensorId: string, actionId: string, parameters: ActionParameters]
|
||||||
close: []
|
close: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -182,7 +188,7 @@ const getUnit = () => {
|
|||||||
const executeAction = async () => {
|
const executeAction = async () => {
|
||||||
isExecuting.value = true
|
isExecuting.value = true
|
||||||
|
|
||||||
const parameters: any = {}
|
const parameters: ActionParameters = {}
|
||||||
|
|
||||||
if (props.action.type === 'adjust') {
|
if (props.action.type === 'adjust') {
|
||||||
if (hasNumericRange.value) {
|
if (hasNumericRange.value) {
|
||||||
@@ -195,7 +201,7 @@ const executeAction = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
emit('execute', props.sensor.id, props.action.id, parameters)
|
emit('execute', props.sensor.sensor_id, props.action.id, parameters)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to execute action:', error)
|
console.error('Failed to execute action:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import {
|
|||||||
roomsApi,
|
roomsApi,
|
||||||
analyticsApi,
|
analyticsApi,
|
||||||
healthApi,
|
healthApi,
|
||||||
type SensorInfo,
|
type SensorDevice,
|
||||||
|
type SensorType,
|
||||||
|
type SensorStatus,
|
||||||
|
type SensorMetadata,
|
||||||
type RoomInfo,
|
type RoomInfo,
|
||||||
type RoomData,
|
type RoomData,
|
||||||
type AnalyticsSummary,
|
type AnalyticsSummary,
|
||||||
@@ -69,23 +72,23 @@ export function useSensorsApi() {
|
|||||||
error: null
|
error: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const sensors = ref<SensorInfo[]>([])
|
const sensors = ref<SensorDevice[]>([])
|
||||||
const currentSensor = ref<SensorInfo | null>(null)
|
const currentSensor = ref<SensorDevice | null>(null)
|
||||||
const sensorData = ref<DataResponse | null>(null)
|
const sensorData = ref<DataResponse | null>(null)
|
||||||
|
|
||||||
const { handleApiCall } = useApi()
|
const { handleApiCall } = useApi()
|
||||||
|
|
||||||
const fetchSensors = async (params?: {
|
const fetchSensors = async (params?: {
|
||||||
room?: string
|
room?: string
|
||||||
sensor_type?: any
|
sensor_type?: SensorType
|
||||||
status?: any
|
status?: SensorStatus
|
||||||
}) => {
|
}) => {
|
||||||
const result = await handleApiCall(
|
const result = await handleApiCall(
|
||||||
() => sensorsApi.getSensors(params),
|
() => sensorsApi.getSensors(params),
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
if (result) {
|
if (result && result.sensors) {
|
||||||
sensors.value = result
|
sensors.value = result.sensors
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -129,7 +132,7 @@ export function useSensorsApi() {
|
|||||||
|
|
||||||
const updateSensorMetadata = async (
|
const updateSensorMetadata = async (
|
||||||
sensorId: string,
|
sensorId: string,
|
||||||
metadata: Record<string, any>
|
metadata: SensorMetadata
|
||||||
) => {
|
) => {
|
||||||
return handleApiCall(
|
return handleApiCall(
|
||||||
() => sensorsApi.updateSensorMetadata(sensorId, metadata),
|
() => sensorsApi.updateSensorMetadata(sensorId, metadata),
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
// Base configuration
|
// Base configuration
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'
|
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
|
// API Response types
|
||||||
export interface ApiResponse<T = any> {
|
export interface ApiResponse<T = unknown> {
|
||||||
data: T
|
data: T
|
||||||
total_count?: number
|
total_count?: number
|
||||||
query?: any
|
query?: Record<string, unknown>
|
||||||
execution_time_ms?: number
|
execution_time_ms?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +79,7 @@ export interface SensorReading {
|
|||||||
value: number
|
value: number
|
||||||
unit: string
|
unit: string
|
||||||
}
|
}
|
||||||
metadata?: Record<string, any>
|
metadata?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoomInfo {
|
export interface RoomInfo {
|
||||||
@@ -169,7 +177,7 @@ export interface SystemEvent {
|
|||||||
event_type: string
|
event_type: string
|
||||||
severity: 'info' | 'warning' | 'error' | 'critical'
|
severity: 'info' | 'warning' | 'error' | 'critical'
|
||||||
message: string
|
message: string
|
||||||
details?: Record<string, any>
|
details?: Record<string, unknown>
|
||||||
sensor_id?: string
|
sensor_id?: string
|
||||||
room?: string
|
room?: string
|
||||||
}
|
}
|
||||||
@@ -187,6 +195,9 @@ export interface SensorDevice {
|
|||||||
actions: SensorAction[]
|
actions: SensorAction[]
|
||||||
}
|
}
|
||||||
metadata: SensorMetadata
|
metadata: SensorMetadata
|
||||||
|
tags?: string[]
|
||||||
|
lastSeen?: number
|
||||||
|
total_readings?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SensorAction {
|
export interface SensorAction {
|
||||||
@@ -231,6 +242,7 @@ export enum SensorStatus {
|
|||||||
ONLINE = 'online',
|
ONLINE = 'online',
|
||||||
OFFLINE = 'offline',
|
OFFLINE = 'offline',
|
||||||
ERROR = 'error',
|
ERROR = 'error',
|
||||||
|
ACTIVE = 'active',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SensorMetadata {
|
export interface SensorMetadata {
|
||||||
@@ -242,6 +254,7 @@ export interface SensorMetadata {
|
|||||||
model?: string
|
model?: string
|
||||||
firmware?: string
|
firmware?: string
|
||||||
battery?: number
|
battery?: number
|
||||||
|
signalStrength?: number
|
||||||
created_at?: string
|
created_at?: string
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
manufacturer?: string
|
manufacturer?: string
|
||||||
@@ -277,7 +290,7 @@ class ApiClient {
|
|||||||
// Dynamically get auth headers to avoid circular imports
|
// Dynamically get auth headers to avoid circular imports
|
||||||
try {
|
try {
|
||||||
// Try to get from window first (for when store is exposed)
|
// 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') {
|
if (authStore && typeof authStore.getAuthHeader === 'function') {
|
||||||
return authStore.getAuthHeader()
|
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}`)
|
const url = new URL(`${this.baseUrl}${endpoint}`)
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
@@ -366,14 +379,14 @@ class ApiClient {
|
|||||||
return await response.json()
|
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, {
|
return this.request<T>(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
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, {
|
return this.request<T>(endpoint, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
|
|||||||
@@ -77,12 +77,22 @@ export const sensorsApi = {
|
|||||||
}): Promise<{
|
}): Promise<{
|
||||||
data: SensorReading[]
|
data: SensorReading[]
|
||||||
count: number
|
count: number
|
||||||
export_params: any
|
export_params: {
|
||||||
|
start_time: number
|
||||||
|
end_time: number
|
||||||
|
sensor_ids?: string
|
||||||
|
format?: 'json' | 'csv'
|
||||||
|
}
|
||||||
}> {
|
}> {
|
||||||
return apiClient.get<{
|
return apiClient.get<{
|
||||||
data: SensorReading[]
|
data: SensorReading[]
|
||||||
count: number
|
count: number
|
||||||
export_params: any
|
export_params: {
|
||||||
|
start_time: number
|
||||||
|
end_time: number
|
||||||
|
sensor_ids?: string
|
||||||
|
format?: 'json' | 'csv'
|
||||||
|
}
|
||||||
}>('/api/v1/export', params)
|
}>('/api/v1/export', params)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ import {
|
|||||||
type HealthCheck,
|
type HealthCheck,
|
||||||
} from '@/services'
|
} from '@/services'
|
||||||
|
|
||||||
|
// Extend Window interface for auth store
|
||||||
|
interface WindowWithAuth extends Window {
|
||||||
|
__AUTH_STORE__?: {
|
||||||
|
ensureAuthenticated: () => Promise<boolean>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const useAnalyticsStore = defineStore('analytics', () => {
|
export const useAnalyticsStore = defineStore('analytics', () => {
|
||||||
// State
|
// State
|
||||||
const analyticsData = ref<{
|
const analyticsData = ref<{
|
||||||
@@ -41,7 +48,7 @@ export const useAnalyticsStore = defineStore('analytics', () => {
|
|||||||
console.warn('Authentication error detected, attempting to re-authenticate...')
|
console.warn('Authentication error detected, attempting to re-authenticate...')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authStore = (window as any).__AUTH_STORE__
|
const authStore = (window as WindowWithAuth).__AUTH_STORE__
|
||||||
if (authStore && typeof authStore.ensureAuthenticated === 'function') {
|
if (authStore && typeof authStore.ensureAuthenticated === 'function') {
|
||||||
const authSuccess = await authStore.ensureAuthenticated()
|
const authSuccess = await authStore.ensureAuthenticated()
|
||||||
if (authSuccess) {
|
if (authSuccess) {
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import { ref, reactive } from 'vue'
|
|||||||
import { roomsApi, type RoomInfo as ApiRoomInfo, type SensorReading } from '@/services'
|
import { roomsApi, type RoomInfo as ApiRoomInfo, type SensorReading } from '@/services'
|
||||||
import { useSensorStore } from './sensor'
|
import { useSensorStore } from './sensor'
|
||||||
|
|
||||||
|
// Extend Window interface for auth store
|
||||||
|
interface WindowWithAuth extends Window {
|
||||||
|
__AUTH_STORE__?: {
|
||||||
|
ensureAuthenticated: () => Promise<boolean>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface RoomMetrics {
|
interface RoomMetrics {
|
||||||
room: string
|
room: string
|
||||||
sensors: string[]
|
sensors: string[]
|
||||||
@@ -221,7 +228,7 @@ export const useRoomStore = defineStore('room', () => {
|
|||||||
console.warn('Authentication error detected, attempting to re-authenticate...')
|
console.warn('Authentication error detected, attempting to re-authenticate...')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authStore = (window as any).__AUTH_STORE__
|
const authStore = (window as WindowWithAuth).__AUTH_STORE__
|
||||||
if (authStore && typeof authStore.ensureAuthenticated === 'function') {
|
if (authStore && typeof authStore.ensureAuthenticated === 'function') {
|
||||||
const authSuccess = await authStore.ensureAuthenticated()
|
const authSuccess = await authStore.ensureAuthenticated()
|
||||||
if (authSuccess) {
|
if (authSuccess) {
|
||||||
|
|||||||
@@ -6,13 +6,21 @@ import {
|
|||||||
type SensorDevice,
|
type SensorDevice,
|
||||||
type SensorStatus,
|
type SensorStatus,
|
||||||
type SensorReading,
|
type SensorReading,
|
||||||
|
type SensorMetadata,
|
||||||
} from '@/services'
|
} from '@/services'
|
||||||
|
|
||||||
|
// Extend Window interface for auth store
|
||||||
|
interface WindowWithAuth extends Window {
|
||||||
|
__AUTH_STORE__?: {
|
||||||
|
ensureAuthenticated: () => Promise<boolean>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const useSensorStore = defineStore('sensor', () => {
|
export const useSensorStore = defineStore('sensor', () => {
|
||||||
// State
|
// State
|
||||||
const sensorDevices = reactive<Map<string, SensorDevice>>(new Map())
|
const sensorDevices = reactive<Map<string, SensorDevice>>(new Map())
|
||||||
const latestReadings = reactive<Map<string, SensorReading>>(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 recentlyUpdatedSensors = reactive<Set<string>>(new Set()) // Track recently updated sensors
|
||||||
const totalReadings = ref<number>(0) // Total number of readings across all sensors
|
const totalReadings = ref<number>(0) // Total number of readings across all sensors
|
||||||
const apiLoading = ref<boolean>(false)
|
const apiLoading = ref<boolean>(false)
|
||||||
@@ -27,6 +35,26 @@ export const useSensorStore = defineStore('sensor', () => {
|
|||||||
).length
|
).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
|
// Actions
|
||||||
function updateSensorRoom(sensorId: string, newRoom: string): void {
|
function updateSensorRoom(sensorId: string, newRoom: string): void {
|
||||||
const sensor = sensorDevices.get(sensorId)
|
const sensor = sensorDevices.get(sensorId)
|
||||||
@@ -93,7 +121,7 @@ export const useSensorStore = defineStore('sensor', () => {
|
|||||||
console.warn('Authentication error detected, attempting to re-authenticate...')
|
console.warn('Authentication error detected, attempting to re-authenticate...')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authStore = (window as any).__AUTH_STORE__
|
const authStore = (window as WindowWithAuth).__AUTH_STORE__
|
||||||
if (authStore && typeof authStore.ensureAuthenticated === 'function') {
|
if (authStore && typeof authStore.ensureAuthenticated === 'function') {
|
||||||
const authSuccess = await authStore.ensureAuthenticated()
|
const authSuccess = await authStore.ensureAuthenticated()
|
||||||
if (authSuccess) {
|
if (authSuccess) {
|
||||||
@@ -193,7 +221,7 @@ export const useSensorStore = defineStore('sensor', () => {
|
|||||||
let totalReadingsCount: number = 0
|
let totalReadingsCount: number = 0
|
||||||
|
|
||||||
result.sensors.forEach((sensor) => {
|
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 sensorType: string = sensor.sensor_type || sensor.type
|
||||||
const sensorName: string = sensor.name || ''
|
const sensorName: string = sensor.name || ''
|
||||||
|
|
||||||
@@ -204,14 +232,12 @@ export const useSensorStore = defineStore('sensor', () => {
|
|||||||
|
|
||||||
const normalizedSensor = {
|
const normalizedSensor = {
|
||||||
...sensor,
|
...sensor,
|
||||||
id: sensorKey,
|
|
||||||
type: sensorType,
|
type: sensorType,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
actions: [], // Default empty actions array
|
|
||||||
monitoring:
|
monitoring:
|
||||||
sensor.capabilities?.monitoring ||
|
sensor.capabilities?.monitoring ||
|
||||||
getDefaultMonitoringCapabilities(sensorType, sensorName),
|
getDefaultMonitoringCapabilities(sensorType, sensorName),
|
||||||
...sensor.capabilities,
|
actions: sensor.capabilities?.actions || [],
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
model: sensor.metadata?.model || 'Unknown',
|
model: sensor.metadata?.model || 'Unknown',
|
||||||
@@ -221,10 +247,10 @@ export const useSensorStore = defineStore('sensor', () => {
|
|||||||
signalStrength: sensor.metadata?.signalStrength,
|
signalStrength: sensor.metadata?.signalStrength,
|
||||||
...sensor.metadata,
|
...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
|
// Update total readings
|
||||||
@@ -246,7 +272,7 @@ export const useSensorStore = defineStore('sensor', () => {
|
|||||||
return handleApiCall(() => sensorsApi.getSensorData(sensorId, params))
|
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))
|
return handleApiCall(() => sensorsApi.updateSensorMetadata(sensorId, metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,6 +302,8 @@ export const useSensorStore = defineStore('sensor', () => {
|
|||||||
// Computed
|
// Computed
|
||||||
totalSensors,
|
totalSensors,
|
||||||
activeSensors,
|
activeSensors,
|
||||||
|
averageCO2Level,
|
||||||
|
maxCO2Level,
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
updateEnergySensors,
|
updateEnergySensors,
|
||||||
|
|||||||
@@ -33,6 +33,18 @@ interface AppSettings {
|
|||||||
developerMode: boolean
|
developerMode: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Type for setting values
|
||||||
|
type SettingValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Theme
|
||||||
|
| Language
|
||||||
|
| NavigationMode
|
||||||
|
| UISettings
|
||||||
|
| NotificationSettings
|
||||||
|
| AppSettings
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: AppSettings = {
|
const DEFAULT_SETTINGS: AppSettings = {
|
||||||
theme: 'system',
|
theme: 'system',
|
||||||
language: 'en',
|
language: 'en',
|
||||||
@@ -93,8 +105,9 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSetting(path: string, value: any) {
|
function updateSetting(path: string, value: SettingValue): void {
|
||||||
const keys = path.split('.')
|
const keys = path.split('.')
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let current: any = settings
|
let current: any = settings
|
||||||
|
|
||||||
for (let i = 0; i < keys.length - 1; i++) {
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
@@ -105,8 +118,9 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSetting(path: string): any {
|
function getSetting(path: string): SettingValue | undefined {
|
||||||
const keys = path.split('.')
|
const keys = path.split('.')
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let current: any = settings
|
let current: any = settings
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
@@ -114,7 +128,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
if (current === undefined) break
|
if (current === undefined) break
|
||||||
}
|
}
|
||||||
|
|
||||||
return current
|
return current as SettingValue | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportSettings(): string {
|
function exportSettings(): string {
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
:content="websocketStore.isConnected ? 'Connected' : 'Disconnected'"
|
:content="websocketStore.isConnected ? 'Connected' : 'Disconnected'"
|
||||||
/>
|
/>
|
||||||
<MetricCard title="Average Usage" :content="averageEnergyUsage" details="kWh" />
|
<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
|
<GraphMetricCard
|
||||||
title="Real-time Energy"
|
title="Real-time Energy"
|
||||||
:content="currentEnergyValue"
|
:content="currentEnergyValue"
|
||||||
@@ -29,18 +31,6 @@
|
|||||||
:trend-data="energyStore.energyHistory.slice(-8)"
|
:trend-data="energyStore.energyHistory.slice(-8)"
|
||||||
trend-direction="neutral"
|
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>
|
||||||
<div>
|
<div>
|
||||||
<RealtimeEnergyChartCard title="Month" />
|
<RealtimeEnergyChartCard title="Month" />
|
||||||
@@ -66,11 +56,13 @@ import SensorConsumptionTable from '@/components/cards/SensorConsumptionTable.vu
|
|||||||
import RoomMetricsCard from '@/components/cards/RoomMetricsCard.vue'
|
import RoomMetricsCard from '@/components/cards/RoomMetricsCard.vue'
|
||||||
import AirQualityCard from '@/components/cards/AirQualityCard.vue'
|
import AirQualityCard from '@/components/cards/AirQualityCard.vue'
|
||||||
import { useEnergyStore } from '@/stores/energy'
|
import { useEnergyStore } from '@/stores/energy'
|
||||||
|
import { useSensorStore } from '@/stores/sensor'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { computed, onMounted, onUnmounted } from 'vue'
|
import { computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { useWebSocketStore } from '@/stores/websocket'
|
import { useWebSocketStore } from '@/stores/websocket'
|
||||||
|
|
||||||
const energyStore = useEnergyStore()
|
const energyStore = useEnergyStore()
|
||||||
|
const sensorStore = useSensorStore()
|
||||||
const websocketStore = useWebSocketStore()
|
const websocketStore = useWebSocketStore()
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
@@ -83,6 +75,14 @@ const averageEnergyUsage = computed(() => {
|
|||||||
return energyStore.averageEnergyUsage.toFixed(2)
|
return energyStore.averageEnergyUsage.toFixed(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const averageCO2 = computed(() => {
|
||||||
|
return Math.round(sensorStore.averageCO2Level)
|
||||||
|
})
|
||||||
|
|
||||||
|
const maxCO2 = computed(() => {
|
||||||
|
return Math.round(sensorStore.maxCO2Level)
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
settingsStore.initialize()
|
settingsStore.initialize()
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|||||||
import { useSensorStore } from '@/stores/sensor'
|
import { useSensorStore } from '@/stores/sensor'
|
||||||
import { useRoomStore } from '@/stores/room'
|
import { useRoomStore } from '@/stores/room'
|
||||||
import { useWebSocketStore } from '@/stores/websocket'
|
import { useWebSocketStore } from '@/stores/websocket'
|
||||||
|
import type { SensorDevice, SensorAction } from '@/services'
|
||||||
import ActionModal from '@/components/modals/ActionModal.vue'
|
import ActionModal from '@/components/modals/ActionModal.vue'
|
||||||
import RoomManagementModal from '@/components/modals/RoomManagementModal.vue'
|
import RoomManagementModal from '@/components/modals/RoomManagementModal.vue'
|
||||||
import SimpleSensorCard from '@/components/cards/SimpleSensorCard.vue'
|
import SimpleSensorCard from '@/components/cards/SimpleSensorCard.vue'
|
||||||
@@ -217,8 +218,8 @@ const selectedType = ref('')
|
|||||||
const selectedStatus = ref('')
|
const selectedStatus = ref('')
|
||||||
|
|
||||||
const showActionModal = ref(false)
|
const showActionModal = ref(false)
|
||||||
const selectedSensor = ref<any>(null)
|
const selectedSensor = ref<SensorDevice | null>(null)
|
||||||
const selectedAction = ref<any>(null)
|
const selectedAction = ref<SensorAction | null>(null)
|
||||||
const isExecutingAction = ref(false)
|
const isExecutingAction = ref(false)
|
||||||
|
|
||||||
const showRoomManagementModal = ref(false)
|
const showRoomManagementModal = ref(false)
|
||||||
@@ -241,17 +242,22 @@ const updateRoom = (sensorId: string, newRoom: string) => {
|
|||||||
sensorStore.updateSensorRoom(sensorId, newRoom)
|
sensorStore.updateSensorRoom(sensorId, newRoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeAction = (sensor: any, action: any) => {
|
const executeAction = (sensor: SensorDevice, action: SensorAction) => {
|
||||||
if (action.parameters) {
|
if (action.parameters) {
|
||||||
selectedSensor.value = sensor
|
selectedSensor.value = sensor
|
||||||
selectedAction.value = action
|
selectedAction.value = action
|
||||||
showActionModal.value = true
|
showActionModal.value = true
|
||||||
} else {
|
} 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
|
isExecutingAction.value = true
|
||||||
try {
|
try {
|
||||||
await sensorStore.executeSensorAction(sensorId, actionId)
|
await sensorStore.executeSensorAction(sensorId, actionId)
|
||||||
|
|||||||
Reference in New Issue
Block a user