diff --git a/src/db/biometrics-migrations/0007_peaceful_blue_shield.sql b/src/db/biometrics-migrations/0007_peaceful_blue_shield.sql new file mode 100644 index 00000000..bd5a0bed --- /dev/null +++ b/src/db/biometrics-migrations/0007_peaceful_blue_shield.sql @@ -0,0 +1,3 @@ +ALTER TABLE `water_level_readings` ADD `raw` integer;--> statement-breakpoint +ALTER TABLE `water_level_readings` ADD `calibrated_empty` integer;--> statement-breakpoint +ALTER TABLE `water_level_readings` ADD `calibrated_full` integer; \ No newline at end of file diff --git a/src/db/biometrics-migrations/meta/0007_snapshot.json b/src/db/biometrics-migrations/meta/0007_snapshot.json new file mode 100644 index 00000000..285740cb --- /dev/null +++ b/src/db/biometrics-migrations/meta/0007_snapshot.json @@ -0,0 +1,896 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "dcd2e360-6812-4ae0-92ed-821a67cba08b", + "prevId": "3ac0eb7e-8dfb-41b1-99a1-41b80065ef0c", + "tables": { + "ambient_light": { + "name": "ambient_light", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lux": { + "name": "lux", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_ambient_light_timestamp": { + "name": "idx_ambient_light_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bed_temp": { + "name": "bed_temp", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ambient_temp": { + "name": "ambient_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcu_temp": { + "name": "mcu_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "humidity": { + "name": "humidity", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "left_outer_temp": { + "name": "left_outer_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "left_center_temp": { + "name": "left_center_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "left_inner_temp": { + "name": "left_inner_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "right_outer_temp": { + "name": "right_outer_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "right_center_temp": { + "name": "right_center_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "right_inner_temp": { + "name": "right_inner_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_bed_temp_timestamp": { + "name": "idx_bed_temp_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "calibration_profiles": { + "name": "calibration_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sensor_type": { + "name": "sensor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "parameters": { + "name": "parameters", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quality_score": { + "name": "quality_score", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_window_start": { + "name": "source_window_start", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_window_end": { + "name": "source_window_end", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "samples_used": { + "name": "samples_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_cal_type_status": { + "name": "idx_cal_type_status", + "columns": [ + "sensor_type", + "status" + ], + "isUnique": false + }, + "uq_cal_side_type_active": { + "name": "uq_cal_side_type_active", + "columns": [ + "side", + "sensor_type" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "calibration_runs": { + "name": "calibration_runs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sensor_type": { + "name": "sensor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parameters": { + "name": "parameters", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quality_score": { + "name": "quality_score", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_window_start": { + "name": "source_window_start", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_window_end": { + "name": "source_window_end", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "samples_used": { + "name": "samples_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_cal_runs_side_type": { + "name": "idx_cal_runs_side_type", + "columns": [ + "side", + "sensor_type", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "flow_readings": { + "name": "flow_readings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "left_flowrate_cd": { + "name": "left_flowrate_cd", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "right_flowrate_cd": { + "name": "right_flowrate_cd", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "left_pump_rpm": { + "name": "left_pump_rpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "right_pump_rpm": { + "name": "right_pump_rpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_flow_readings_timestamp": { + "name": "idx_flow_readings_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "freezer_temp": { + "name": "freezer_temp", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ambient_temp": { + "name": "ambient_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "heatsink_temp": { + "name": "heatsink_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "left_water_temp": { + "name": "left_water_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "right_water_temp": { + "name": "right_water_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_freezer_temp_timestamp": { + "name": "idx_freezer_temp_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "movement": { + "name": "movement", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_movement": { + "name": "total_movement", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_movement_side_timestamp": { + "name": "idx_movement_side_timestamp", + "columns": [ + "side", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sleep_records": { + "name": "sleep_records", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entered_bed_at": { + "name": "entered_bed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "left_bed_at": { + "name": "left_bed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sleep_duration_seconds": { + "name": "sleep_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "times_exited_bed": { + "name": "times_exited_bed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "present_intervals": { + "name": "present_intervals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_present_intervals": { + "name": "not_present_intervals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_sleep_records_side_entered": { + "name": "idx_sleep_records_side_entered", + "columns": [ + "side", + "entered_bed_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vitals": { + "name": "vitals", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heart_rate": { + "name": "heart_rate", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hrv": { + "name": "hrv", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "breathing_rate": { + "name": "breathing_rate", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vitals_side_timestamp": { + "name": "idx_vitals_side_timestamp", + "columns": [ + "side", + "timestamp" + ], + "isUnique": false + }, + "uq_vitals_side_timestamp": { + "name": "uq_vitals_side_timestamp", + "columns": [ + "side", + "timestamp" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vitals_quality": { + "name": "vitals_quality", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "vitals_id": { + "name": "vitals_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quality_score": { + "name": "quality_score", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "flags": { + "name": "flags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hr_raw": { + "name": "hr_raw", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_vq_vitals_id": { + "name": "idx_vq_vitals_id", + "columns": [ + "vitals_id" + ], + "isUnique": false + }, + "idx_vq_side_ts": { + "name": "idx_vq_side_ts", + "columns": [ + "side", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "water_level_alerts": { + "name": "water_level_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_water_level_alerts_dismissed": { + "name": "idx_water_level_alerts_dismissed", + "columns": [ + "dismissed_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "water_level_readings": { + "name": "water_level_readings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw": { + "name": "raw", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "calibrated_empty": { + "name": "calibrated_empty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "calibrated_full": { + "name": "calibrated_full", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_water_level_timestamp": { + "name": "idx_water_level_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/db/biometrics-migrations/meta/_journal.json b/src/db/biometrics-migrations/meta/_journal.json index c886afab..03e305a1 100644 --- a/src/db/biometrics-migrations/meta/_journal.json +++ b/src/db/biometrics-migrations/meta/_journal.json @@ -50,6 +50,13 @@ "when": 1774803436479, "tag": "0006_tired_gressill", "breakpoints": true + }, + { + "idx": 7, + "version": "6", + "when": 1775457794262, + "tag": "0007_peaceful_blue_shield", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/biometrics-schema.ts b/src/db/biometrics-schema.ts index 793b0755..4615da59 100644 --- a/src/db/biometrics-schema.ts +++ b/src/db/biometrics-schema.ts @@ -81,6 +81,9 @@ export const waterLevelReadings = sqliteTable('water_level_readings', { id: integer('id').primaryKey({ autoIncrement: true }), timestamp: integer('timestamp', { mode: 'timestamp' }).notNull(), level: text('level', { enum: ['low', 'ok'] }).notNull(), + raw: integer('raw'), + calibratedEmpty: integer('calibrated_empty'), + calibratedFull: integer('calibrated_full'), }, t => [ uniqueIndex('idx_water_level_timestamp').on(t.timestamp), ]) diff --git a/src/hardware/wifi.ts b/src/hardware/wifi.ts new file mode 100644 index 00000000..2591fe7e --- /dev/null +++ b/src/hardware/wifi.ts @@ -0,0 +1,47 @@ +import { readFileSync } from 'fs' +import { spawnSync } from 'child_process' + +export interface WifiInfo { + wifiStrength: number + wifiSSID: string +} + +export function getWifiInfo(): WifiInfo { + try { + const wifiStrength = parseSignalStrength() + const wifiSSID = parseSSID() + return { wifiStrength, wifiSSID } + } + catch { + return { wifiStrength: -1, wifiSSID: 'unknown' } + } +} + +function parseSignalStrength(): number { + try { + const raw = readFileSync('/proc/net/wireless', 'utf-8') + const lines = raw.trim().split('\n') + // Skip header lines, parse the interface line + const dataLine = lines.find(l => l.includes(':')) + if (!dataLine) return -1 + const parts = dataLine.trim().split(/\s+/) + // Format: iface | status | link | level | noise ... + const link = parseFloat(parts[2]) + if (isNaN(link)) return -1 + // link quality is 0-70 on Linux, normalize to 0-100 + return Math.round(Math.min(100, (link / 70) * 100)) + } + catch { + return -1 + } +} + +function parseSSID(): string { + try { + const result = spawnSync('iwgetid', ['-r'], { encoding: 'utf-8', timeout: 2000 }) + return result.stdout?.trim() || 'unknown' + } + catch { + return 'unknown' + } +} diff --git a/src/server/routers/device.ts b/src/server/routers/device.ts index 73075f76..4ecafd5d 100644 --- a/src/server/routers/device.ts +++ b/src/server/routers/device.ts @@ -1,9 +1,10 @@ import { z } from 'zod' import { TRPCError } from '@trpc/server' import { publicProcedure, router } from '@/src/server/trpc' -import { db } from '@/src/db' +import { db, biometricsDb } from '@/src/db' import { deviceState } from '@/src/db/schema' -import { eq } from 'drizzle-orm' +import { bedTemp, waterLevelReadings } from '@/src/db/biometrics-schema' +import { eq, desc } from 'drizzle-orm' import { withHardwareClient } from '@/src/server/helpers' import { getPrimeCompletedAt, dismissPrimeNotification } from '@/src/hardware/primeNotification' import { snoozeAlarm, cancelSnooze, getSnoozeStatus } from '@/src/hardware/snoozeManager' @@ -17,7 +18,8 @@ import { vibrationPatternSchema, alarmDurationSchema, } from '@/src/server/validation-schemas' -import { toC } from '@/src/lib/tempUtils' +import { toC, centiDegreesToC, centiPercentToPercent } from '@/src/lib/tempUtils' +import { getWifiInfo } from '@/src/hardware/wifi' // --------------------------------------------------------------------------- // Command name → HardwareCommand mapping for the raw execute endpoint @@ -142,6 +144,36 @@ export const deviceRouter = router({ const convertTemp = (f: number) => input.unit === 'C' ? Math.round(toC(f) * 10) / 10 : f + // Best-effort enrichment — nulls on failure + let wifiStrength: number = -1 + let wifiSSID: string = 'unknown' + let roomClimate: { temperatureC: number | null, humidity: number | null, timestamp: number | null } = { temperatureC: null, humidity: null, timestamp: null } + let waterLevelRaw: { raw: number | null, calibratedEmpty: number | null, calibratedFull: number | null, timestamp: number | null } = { raw: null, calibratedEmpty: null, calibratedFull: null, timestamp: null } + try { + const wifi = getWifiInfo() + wifiStrength = wifi.wifiStrength + wifiSSID = wifi.wifiSSID + + const [latestBed] = await biometricsDb.select().from(bedTemp).orderBy(desc(bedTemp.timestamp)).limit(1) + if (latestBed) { + roomClimate = { + temperatureC: latestBed.ambientTemp !== null ? centiDegreesToC(latestBed.ambientTemp) : null, + humidity: latestBed.humidity !== null ? centiPercentToPercent(latestBed.humidity) : null, + timestamp: latestBed.timestamp ? latestBed.timestamp.getTime() : null, + } + } + const [latestWater] = await biometricsDb.select().from(waterLevelReadings).orderBy(desc(waterLevelReadings.timestamp)).limit(1) + if (latestWater) { + waterLevelRaw = { + raw: latestWater.raw ?? null, + calibratedEmpty: latestWater.calibratedEmpty ?? null, + calibratedFull: latestWater.calibratedFull ?? null, + timestamp: latestWater.timestamp ? latestWater.timestamp.getTime() : null, + } + } + } + catch { /* enrichment is best-effort */ } + return { ...status, leftSide: { @@ -156,6 +188,10 @@ export const deviceRouter = router({ }, ...(primeCompletedAt && { primeCompletedNotification: { timestamp: primeCompletedAt } }), snooze: { left: leftSnooze, right: rightSnooze }, + wifiStrength, + wifiSSID, + roomClimate, + waterLevelRaw, } }, 'Failed to get device status') }),