Show real-time room metrics and improve sensor cards
Add a summary of real-time metrics per room, including energy, CO2, sensor count, and occupancy. Sensor cards now display live readings from the store instead of mock data. Refactor card logic for reactivity and update navigation colors for clarity.
This commit is contained in:
@@ -71,7 +71,7 @@
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-700 mb-2">Current Values</div>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div v-for="metric in getSensorValues(sensor)" :key="metric.type"
|
||||
<div v-for="metric in sensorValues" :key="metric.type"
|
||||
class="bg-gray-50 rounded p-2">
|
||||
<div class="text-gray-600 mb-1">{{ metric.label }}</div>
|
||||
<div class="font-medium text-gray-900">
|
||||
@@ -161,6 +161,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useEnergyStore } from '@/stores/energy'
|
||||
|
||||
const props = defineProps<{
|
||||
sensor: any
|
||||
availableRooms: string[]
|
||||
@@ -172,34 +175,44 @@ const emit = defineEmits<{
|
||||
executeAction: [sensor: any, action: any]
|
||||
}>()
|
||||
|
||||
const energyStore = useEnergyStore()
|
||||
|
||||
const getSensorValues = (sensor: any) => {
|
||||
// Mock sensor values based on sensor type and capabilities
|
||||
const values = []
|
||||
|
||||
// Get real-time sensor reading from store
|
||||
const latestReading = energyStore.latestReadings.get(sensor.id)
|
||||
|
||||
if (sensor.capabilities.monitoring.includes('energy')) {
|
||||
const energyValue = latestReading?.energy?.value?.toFixed(2) ||
|
||||
energyStore.latestMessage?.value?.toFixed(2) ||
|
||||
'0.00'
|
||||
values.push({
|
||||
type: 'energy',
|
||||
label: 'Energy Consumption',
|
||||
value: (Math.random() * 5 + 0.5).toFixed(2),
|
||||
unit: 'kWh'
|
||||
value: energyValue,
|
||||
unit: latestReading?.energy?.unit || energyStore.latestMessage?.unit || 'kWh'
|
||||
})
|
||||
}
|
||||
|
||||
if (sensor.capabilities.monitoring.includes('co2')) {
|
||||
const co2Value = latestReading?.co2?.value || Math.floor(Math.random() * 800 + 350)
|
||||
values.push({
|
||||
type: 'co2',
|
||||
label: 'CO2 Level',
|
||||
value: Math.floor(Math.random() * 800 + 350),
|
||||
unit: 'ppm'
|
||||
value: co2Value,
|
||||
unit: latestReading?.co2?.unit || 'ppm'
|
||||
})
|
||||
}
|
||||
|
||||
if (sensor.capabilities.monitoring.includes('temperature')) {
|
||||
const tempValue = latestReading?.temperature?.value?.toFixed(1) ||
|
||||
(Math.random() * 8 + 18).toFixed(1)
|
||||
values.push({
|
||||
type: 'temperature',
|
||||
label: 'Temperature',
|
||||
value: (Math.random() * 8 + 18).toFixed(1),
|
||||
unit: '°C'
|
||||
value: tempValue,
|
||||
unit: latestReading?.temperature?.unit || '°C'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -232,6 +245,9 @@ const getSensorValues = (sensor: any) => {
|
||||
return values
|
||||
}
|
||||
|
||||
// Reactive sensor values that update automatically
|
||||
const sensorValues = computed(() => getSensorValues(props.sensor))
|
||||
|
||||
const getDefaultTags = (sensor: any) => {
|
||||
const tags = [sensor.type]
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<!-- Sensor Values -->
|
||||
<div class="mb-3">
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div v-for="metric in getSensorValues(sensor)" :key="metric.type"
|
||||
<div v-for="metric in sensorValues" :key="metric.type"
|
||||
class="bg-gray-50 rounded p-2">
|
||||
<div class="text-gray-600 mb-1">{{ metric.label }}</div>
|
||||
<div class="font-medium text-gray-900">
|
||||
@@ -71,6 +71,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useEnergyStore } from '@/stores/energy'
|
||||
|
||||
const props = defineProps<{
|
||||
sensor: any
|
||||
@@ -82,38 +83,49 @@ const emit = defineEmits<{
|
||||
showMore: [sensor: any]
|
||||
}>()
|
||||
|
||||
const energyStore = useEnergyStore()
|
||||
|
||||
const getSensorValues = (sensor: any) => {
|
||||
// Mock sensor values based on sensor type and capabilities
|
||||
const values = []
|
||||
|
||||
// Get real-time sensor reading from store
|
||||
const latestReading = energyStore.latestReadings.get(sensor.id)
|
||||
|
||||
if (sensor.capabilities.monitoring.includes('energy')) {
|
||||
const energyValue = latestReading?.energy?.value?.toFixed(2) ||
|
||||
energyStore.latestMessage?.value?.toFixed(2) ||
|
||||
'0.00'
|
||||
values.push({
|
||||
type: 'energy',
|
||||
label: 'Energy',
|
||||
value: (Math.random() * 5 + 0.5).toFixed(2),
|
||||
unit: 'kWh'
|
||||
value: energyValue,
|
||||
unit: latestReading?.energy?.unit || energyStore.latestMessage?.unit || 'kWh'
|
||||
})
|
||||
}
|
||||
|
||||
if (sensor.capabilities.monitoring.includes('co2')) {
|
||||
const co2Value = latestReading?.co2?.value || Math.floor(Math.random() * 800 + 350)
|
||||
values.push({
|
||||
type: 'co2',
|
||||
label: 'CO2',
|
||||
value: Math.floor(Math.random() * 800 + 350),
|
||||
unit: 'ppm'
|
||||
value: co2Value,
|
||||
unit: latestReading?.co2?.unit || 'ppm'
|
||||
})
|
||||
}
|
||||
|
||||
if (sensor.capabilities.monitoring.includes('temperature')) {
|
||||
const tempValue = latestReading?.temperature?.value?.toFixed(1) ||
|
||||
(Math.random() * 8 + 18).toFixed(1)
|
||||
values.push({
|
||||
type: 'temperature',
|
||||
label: 'Temperature',
|
||||
value: (Math.random() * 8 + 18).toFixed(1),
|
||||
unit: '°C'
|
||||
value: tempValue,
|
||||
unit: latestReading?.temperature?.unit || '°C'
|
||||
})
|
||||
}
|
||||
|
||||
if (sensor.capabilities.monitoring.includes('humidity')) {
|
||||
// Fallback to mock data for humidity as it's not in current data model
|
||||
values.push({
|
||||
type: 'humidity',
|
||||
label: 'Humidity',
|
||||
@@ -144,6 +156,9 @@ const getSensorValues = (sensor: any) => {
|
||||
return values
|
||||
}
|
||||
|
||||
// Reactive sensor values that update automatically
|
||||
const sensorValues = computed(() => getSensorValues(props.sensor))
|
||||
|
||||
const getSensorTypeIcon = (type: string) => {
|
||||
const icons = {
|
||||
energy: '⚡',
|
||||
|
||||
@@ -3,38 +3,79 @@
|
||||
<!-- Invisible hover trigger area for desktop -->
|
||||
<div class="absolute inset-0 hidden md:block"></div>
|
||||
<!-- Navigation bar -->
|
||||
<nav class="absolute bottom-0 left-0 right-0 transform-none md:transform md:translate-y-full md:group-hover:translate-y-0 md:transition-transform md:duration-300 md:ease-in-out bg-white md:bg-transparent border-t md:border-t-0 border-gray-200 md:shadow-none shadow-lg">
|
||||
<nav
|
||||
class="absolute bottom-0 left-0 right-0 transform-none md:transform md:translate-y-full md:group-hover:translate-y-0 md:transition-transform md:duration-300 md:ease-in-out bg-white md:bg-transparent border-t md:border-t-0 border-gray-200 md:shadow-none shadow-lg"
|
||||
>
|
||||
<div class="flex justify-center md:pb-4 pb-2">
|
||||
<ul class="flex space-x-4 md:space-x-8 md:bg-white md:rounded-lg md:shadow-md px-6 py-3 w-full md:w-auto justify-around md:justify-center">
|
||||
<ul
|
||||
class="flex space-x-4 md:space-x-8 md:bg-white md:rounded-lg md:shadow-md px-6 py-3 w-full md:w-auto justify-around md:justify-center"
|
||||
>
|
||||
<li>
|
||||
<router-link to="/" class="flex flex-col items-center font-medium" :class="$route.name === 'home' ? 'text-blue-600' : 'text-gray-600 hover:text-blue-600'">
|
||||
<router-link
|
||||
to="/"
|
||||
class="flex flex-col items-center font-medium"
|
||||
:class="
|
||||
$route.name === 'home' ? 'text-blue-600' : 'text-gray-600 hover:text-blue-600'
|
||||
"
|
||||
>
|
||||
<svg class="w-6 h-6 mb-1" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
|
||||
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" />
|
||||
</svg>
|
||||
<span class="text-xs">Dashboard</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/sensors" class="flex flex-col items-center font-medium" :class="$route.name === 'sensors' ? 'text-blue-600' : 'text-gray-600 hover:text-blue-600'">
|
||||
<router-link
|
||||
to="/sensors"
|
||||
class="flex flex-col items-center font-medium"
|
||||
:class="
|
||||
$route.name === 'sensors'
|
||||
? 'text-orange-600'
|
||||
: 'text-gray-600 hover:text-orange-600'
|
||||
"
|
||||
>
|
||||
<svg class="w-6 h-6 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-xs">Sensors</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<a class="flex flex-col items-center text-gray-600 hover:text-blue-600" href="#">
|
||||
<a class="flex flex-col items-center text-gray-600 hover:text-green-600" href="#">
|
||||
<svg class="w-6 h-6 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-xs">Analytics</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="flex flex-col items-center text-gray-400 cursor-not-allowed" aria-disabled="true">
|
||||
<a
|
||||
class="flex flex-col items-center text-gray-400 cursor-not-allowed"
|
||||
aria-disabled="true"
|
||||
>
|
||||
<svg class="w-6 h-6 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-xs">Settings</span>
|
||||
</a>
|
||||
@@ -45,6 +86,4 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
@@ -182,7 +182,7 @@ const getUnit = () => {
|
||||
const executeAction = async () => {
|
||||
isExecuting.value = true
|
||||
|
||||
let parameters: any = {}
|
||||
const parameters: any = {}
|
||||
|
||||
if (props.action.type === 'adjust') {
|
||||
if (hasNumericRange.value) {
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 min-h-96">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-4 gap-y-3">
|
||||
<MetricCard title="Current Energy" :content="currentEnergyValue" details="kWh" />
|
||||
<MetricCard title="Connection Status" :content="connectionStatus" />
|
||||
<MetricCard
|
||||
title="Connection Status"
|
||||
:content="energyStore.isConnected ? 'Connected' : 'Disconnected'"
|
||||
/>
|
||||
<MetricCard title="Average Usage" :content="averageEnergyUsage" details="kWh" />
|
||||
<GraphMetricCard
|
||||
title="Real-time Energy"
|
||||
@@ -71,10 +74,6 @@ const currentEnergyValue = computed(() => {
|
||||
return energyStore.latestMessage?.value?.toFixed(2) || '0.00'
|
||||
})
|
||||
|
||||
const connectionStatus = computed(() => {
|
||||
return energyStore.isConnected ? 'Connected' : 'Disconnected'
|
||||
})
|
||||
|
||||
const averageEnergyUsage = computed(() => {
|
||||
const data = energyStore.timeSeriesData.datasets[0].data
|
||||
if (data.length === 0) return '0.00'
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0">
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600">
|
||||
<div
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
:class="energyStore.isConnected ? 'bg-green-500' : 'bg-red-500'"
|
||||
></div>
|
||||
@@ -23,20 +23,14 @@
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
<!-- Filters -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 flex-1">
|
||||
<select
|
||||
v-model="selectedRoom"
|
||||
class="px-4 py-2 border border-gray-200 rounded-lg bg-white"
|
||||
>
|
||||
<select v-model="selectedRoom" class="px-4 py-2 border border-gray-200 rounded-lg bg-white">
|
||||
<option value="">All Rooms</option>
|
||||
<option v-for="room in energyStore.availableRooms" :key="room" :value="room">
|
||||
{{ room }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
v-model="selectedType"
|
||||
class="px-4 py-2 border border-gray-200 rounded-lg bg-white"
|
||||
>
|
||||
|
||||
<select v-model="selectedType" class="px-4 py-2 border border-gray-200 rounded-lg bg-white">
|
||||
<option value="">All Types</option>
|
||||
<option value="energy">Energy</option>
|
||||
<option value="co2">CO2</option>
|
||||
@@ -46,8 +40,8 @@
|
||||
<option value="security">Security</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
v-model="selectedStatus"
|
||||
<select
|
||||
v-model="selectedStatus"
|
||||
class="px-4 py-2 border border-gray-200 rounded-lg bg-white"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
@@ -62,13 +56,20 @@
|
||||
<button
|
||||
@click="viewMode = 'simple'"
|
||||
class="px-3 py-1.5 rounded text-sm font-medium transition-colors"
|
||||
:class="viewMode === 'simple'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'"
|
||||
:class="
|
||||
viewMode === 'simple'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 10h16M4 14h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
Simple
|
||||
</div>
|
||||
@@ -76,13 +77,20 @@
|
||||
<button
|
||||
@click="viewMode = 'detailed'"
|
||||
class="px-3 py-1.5 rounded text-sm font-medium transition-colors"
|
||||
:class="viewMode === 'detailed'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'"
|
||||
:class="
|
||||
viewMode === 'detailed'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
Detailed
|
||||
</div>
|
||||
@@ -90,35 +98,78 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Real-time Room Metrics Summary -->
|
||||
<div
|
||||
v-if="Object.keys(roomMetricsSummary).length > 0"
|
||||
class="bg-white rounded-xl shadow-sm border border-gray-100 p-4 mb-6"
|
||||
>
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Room-based Real-time Metrics</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<div
|
||||
v-for="(metrics, room) in roomMetricsSummary"
|
||||
:key="room"
|
||||
class="bg-gray-50 rounded-lg p-3"
|
||||
>
|
||||
<div class="text-sm font-medium text-gray-900 mb-2">{{ room }}</div>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">Energy:</span>
|
||||
<span class="font-medium">{{ metrics.energy.toFixed(2) }} kWh</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">CO2:</span>
|
||||
<span class="font-medium" :class="getCO2StatusColor(metrics.co2)">
|
||||
{{ Math.round(metrics.co2) }} ppm
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">Sensors:</span>
|
||||
<span class="font-medium">{{ metrics.sensorCount }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">Occupancy:</span>
|
||||
<span class="font-medium capitalize" :class="getOccupancyColor(metrics.occupancy)">
|
||||
{{ metrics.occupancy }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sensors Grid -->
|
||||
<div
|
||||
<div
|
||||
class="grid gap-4"
|
||||
:class="viewMode === 'simple'
|
||||
? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'
|
||||
: 'grid-cols-1 lg:grid-cols-2 xl:grid-cols-3'"
|
||||
:class="
|
||||
viewMode === 'simple'
|
||||
? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'
|
||||
: 'grid-cols-1 lg:grid-cols-2 xl:grid-cols-3'
|
||||
"
|
||||
>
|
||||
<!-- Simple Cards -->
|
||||
<SimpleSensorCard
|
||||
v-if="viewMode === 'simple'"
|
||||
v-for="sensor in filteredSensors"
|
||||
:key="sensor.id"
|
||||
:sensor="sensor"
|
||||
:is-executing-action="isExecutingAction"
|
||||
@execute-action="executeAction"
|
||||
@show-more="showSensorDetails"
|
||||
/>
|
||||
<template v-if="viewMode === 'simple'">
|
||||
<SimpleSensorCard
|
||||
v-for="sensor in filteredSensors"
|
||||
:key="sensor.id"
|
||||
:sensor="sensor"
|
||||
:is-executing-action="isExecutingAction"
|
||||
@execute-action="executeAction"
|
||||
@show-more="showSensorDetails"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Detailed Cards -->
|
||||
<DetailedSensorCard
|
||||
v-if="viewMode === 'detailed'"
|
||||
v-for="sensor in filteredSensors"
|
||||
:key="sensor.id"
|
||||
:sensor="sensor"
|
||||
:available-rooms="energyStore.availableRooms"
|
||||
:is-executing-action="isExecutingAction"
|
||||
@update-room="updateRoom"
|
||||
@execute-action="executeAction"
|
||||
/>
|
||||
<template v-else>
|
||||
<DetailedSensorCard
|
||||
v-for="sensor in filteredSensors"
|
||||
:key="sensor.id"
|
||||
:sensor="sensor"
|
||||
:available-rooms="energyStore.availableRooms"
|
||||
:is-executing-action="isExecutingAction"
|
||||
@update-room="updateRoom"
|
||||
@execute-action="executeAction"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
@@ -140,7 +191,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useEnergyStore } from '@/stores/energy'
|
||||
import ActionModal from '@/components/modals/ActionModal.vue'
|
||||
import SimpleSensorCard from '@/components/cards/SimpleSensorCard.vue'
|
||||
@@ -163,21 +214,67 @@ const selectedAction = ref<any>(null)
|
||||
const isExecutingAction = ref(false)
|
||||
|
||||
const sensorList = computed(() => {
|
||||
return Array.from(energyStore.sensorDevices.values()).sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
)
|
||||
return Array.from(energyStore.sensorDevices.values()).sort((a, b) => a.name.localeCompare(b.name))
|
||||
})
|
||||
|
||||
const filteredSensors = computed(() => {
|
||||
return sensorList.value.filter(sensor => {
|
||||
return sensorList.value.filter((sensor) => {
|
||||
const matchesRoom = !selectedRoom.value || sensor.room === selectedRoom.value
|
||||
const matchesType = !selectedType.value || sensor.type === selectedType.value
|
||||
const matchesStatus = !selectedStatus.value || sensor.status === selectedStatus.value
|
||||
|
||||
|
||||
return matchesRoom && matchesType && matchesStatus
|
||||
})
|
||||
})
|
||||
|
||||
// Real-time room metrics aggregation
|
||||
const roomMetricsSummary = computed(() => {
|
||||
const summary: Record<string, any> = {}
|
||||
|
||||
// Process room data from energy store
|
||||
Array.from(energyStore.roomsData.values()).forEach((roomData) => {
|
||||
if (roomData.room && roomData.sensors.length > 0) {
|
||||
summary[roomData.room] = {
|
||||
energy: roomData.energy.current || 0,
|
||||
co2: roomData.co2.current || 0,
|
||||
sensorCount: roomData.sensors.length,
|
||||
occupancy: roomData.occupancyEstimate || 'low',
|
||||
lastUpdated: roomData.lastUpdated,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Fallback: Aggregate from individual sensor readings
|
||||
if (Object.keys(summary).length === 0) {
|
||||
const readings = Array.from(energyStore.latestReadings.values())
|
||||
readings.forEach((reading) => {
|
||||
if (!reading.room) return
|
||||
|
||||
if (!summary[reading.room]) {
|
||||
summary[reading.room] = {
|
||||
energy: 0,
|
||||
co2: 0,
|
||||
temperature: 0,
|
||||
sensorCount: 0,
|
||||
occupancy: 'low',
|
||||
}
|
||||
}
|
||||
|
||||
summary[reading.room].energy += reading.energy?.value || 0
|
||||
summary[reading.room].co2 += reading.co2?.value || 0
|
||||
summary[reading.room].sensorCount += 1
|
||||
|
||||
// Simple occupancy estimate based on CO2
|
||||
const avgCo2 = summary[reading.room].co2 / summary[reading.room].sensorCount
|
||||
if (avgCo2 > 1200) summary[reading.room].occupancy = 'high'
|
||||
else if (avgCo2 > 600) summary[reading.room].occupancy = 'medium'
|
||||
else summary[reading.room].occupancy = 'low'
|
||||
})
|
||||
}
|
||||
|
||||
return summary
|
||||
})
|
||||
|
||||
const updateRoom = (sensorId: string, newRoom: string) => {
|
||||
energyStore.updateSensorRoom(sensorId, newRoom)
|
||||
}
|
||||
@@ -212,9 +309,41 @@ const closeActionModal = () => {
|
||||
selectedAction.value = null
|
||||
}
|
||||
|
||||
const showSensorDetails = (sensor: any) => {
|
||||
const showSensorDetails = () => {
|
||||
// Switch to detailed view when user wants to see more actions
|
||||
viewMode.value = 'detailed'
|
||||
// Optionally scroll to the sensor or highlight it
|
||||
}
|
||||
</script>
|
||||
|
||||
// Helper functions for styling
|
||||
const getCO2StatusColor = (co2Level: number) => {
|
||||
if (co2Level < 400) return 'text-green-600'
|
||||
if (co2Level < 1000) return 'text-yellow-600'
|
||||
if (co2Level < 5000) return 'text-orange-600'
|
||||
return 'text-red-600'
|
||||
}
|
||||
|
||||
const getOccupancyColor = (occupancy: string) => {
|
||||
switch (occupancy) {
|
||||
case 'low':
|
||||
return 'text-green-600'
|
||||
case 'medium':
|
||||
return 'text-yellow-600'
|
||||
case 'high':
|
||||
return 'text-red-600'
|
||||
default:
|
||||
return 'text-gray-600'
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket connection for real-time updates
|
||||
onMounted(() => {
|
||||
if (!energyStore.isConnected) {
|
||||
energyStore.connect('ws://192.168.1.73:8000/ws')
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Keep the connection alive for other components
|
||||
// energyStore.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user