From 6f2045d2a017f22e0dafd0ceb8296bc6c9b46ab5 Mon Sep 17 00:00:00 2001 From: Bob van de Vijver Date: Wed, 28 Aug 2024 10:35:56 +0200 Subject: [PATCH 1/2] Promote safeSetCapabilityValue function to device level --- drivers/dimmer/device.ts | 14 ++++---------- drivers/socket/device.ts | 16 +++++----------- lib/TuyaOAuth2Device.ts | 10 +++++++++- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/drivers/dimmer/device.ts b/drivers/dimmer/device.ts index 1f120cf5..ad5bbe1d 100644 --- a/drivers/dimmer/device.ts +++ b/drivers/dimmer/device.ts @@ -29,12 +29,6 @@ export default class TuyaOAuth2DeviceDimmer extends TuyaOAuth2Device { } } - async safeSetCapabilityValue(capabilityId: string, value: unknown): Promise { - if (this.hasCapability(capabilityId)) { - await this.setCapabilityValue(capabilityId, value); - } - } - async onTuyaStatus(status: TuyaStatus, changed: string[]): Promise { await super.onTuyaStatus(status, changed); @@ -63,7 +57,7 @@ export default class TuyaOAuth2DeviceDimmer extends TuyaOAuth2Device { triggerCard.trigger(this, {}, {}).catch(this.error); } - await this.safeSetCapabilityValue(`onoff.${switch_i}`, switchStatus).catch(this.error); + await this.safeSetCapabilityValue(`onoff.${switch_i}`, switchStatus); } if (typeof brightnessMin === 'number') { @@ -98,12 +92,12 @@ export default class TuyaOAuth2DeviceDimmer extends TuyaOAuth2Device { .catch(this.error); } - await this.safeSetCapabilityValue(`dim.${switch_i}`, scaledValue).catch(this.error); - await this.safeSetCapabilityValue(`dim`, scaledValue).catch(this.error); + await this.safeSetCapabilityValue(`dim.${switch_i}`, scaledValue); + await this.safeSetCapabilityValue(`dim`, scaledValue); } } - await this.safeSetCapabilityValue('onoff', anySwitchOn).catch(this.error); + await this.safeSetCapabilityValue('onoff', anySwitchOn); } // TODO migrate to util onSettings diff --git a/drivers/socket/device.ts b/drivers/socket/device.ts index 34d96c31..81752893 100644 --- a/drivers/socket/device.ts +++ b/drivers/socket/device.ts @@ -32,12 +32,6 @@ export default class TuyaOAuth2DeviceSocket extends TuyaOAuth2Device { } } - async safeSetCapabilityValue(capabilityId: string, value: unknown): Promise { - if (this.hasCapability(capabilityId)) { - await this.setCapabilityValue(capabilityId, value); - } - } - async onTuyaStatus(status: TuyaStatus, changedStatusCodes: string[]): Promise { await super.onTuyaStatus(status, changedStatusCodes); @@ -66,7 +60,7 @@ export default class TuyaOAuth2DeviceSocket extends TuyaOAuth2Device { .catch(this.error); } - this.safeSetCapabilityValue(switchCapability, switchStatus).catch(this.error); + await this.safeSetCapabilityValue(switchCapability, switchStatus); } } @@ -74,22 +68,22 @@ export default class TuyaOAuth2DeviceSocket extends TuyaOAuth2Device { anySwitchOn = anySwitchOn || status['switch']; } - this.safeSetCapabilityValue('onoff', anySwitchOn).catch(this.error); + await this.safeSetCapabilityValue('onoff', anySwitchOn); if (typeof status['cur_power'] === 'number') { const scaling = 10.0 ** parseInt(this.getSetting('power_scaling') ?? '0'); - this.setCapabilityValue('measure_power', status['cur_power'] / scaling).catch(this.error); + await this.safeSetCapabilityValue('measure_power', status['cur_power'] / scaling); } if (typeof status['cur_voltage'] === 'number') { const scaling = 10.0 ** parseInt(this.getSetting('cur_voltage_scaling') ?? '0'); - this.setCapabilityValue('measure_voltage', status['cur_voltage'] / scaling).catch(this.error); + await this.safeSetCapabilityValue('measure_voltage', status['cur_voltage'] / scaling); } if (typeof status['cur_current'] === 'number') { // Additionally convert mA const scaling = 1000.0 * 10.0 ** parseInt(this.getSetting('cur_current_scaling') ?? '0'); - this.setCapabilityValue('measure_current', status['cur_current'] / scaling).catch(this.error); + await this.safeSetCapabilityValue('measure_current', status['cur_current'] / scaling); } if (status['child_lock'] !== undefined) { diff --git a/lib/TuyaOAuth2Device.ts b/lib/TuyaOAuth2Device.ts index 8c1ce2ba..d8d0908c 100644 --- a/lib/TuyaOAuth2Device.ts +++ b/lib/TuyaOAuth2Device.ts @@ -217,7 +217,7 @@ export default class TuyaOAuth2Device extends OAuth2Device { return this.oAuth2Client.queryDataPoints(deviceId); } - setDataPoint(dataPointId: string, value: unknown): Promise { + async setDataPoint(dataPointId: string, value: unknown): Promise { const { deviceId } = this.data; return this.oAuth2Client.setDataPoint(deviceId, dataPointId, value); } @@ -232,6 +232,14 @@ export default class TuyaOAuth2Device extends OAuth2Device { return this.oAuth2Client.getStreamingLink(deviceId, type); } + async safeSetCapabilityValue(capabilityId: string, value: unknown): Promise { + if (!this.hasCapability(capabilityId)) { + return; + } + + await this.setCapabilityValue(capabilityId, value).catch(this.error); + } + log(...args: unknown[]): void { super.log(`[tc:${this.getStoreValue('tuya_category')}]`, ...args); } From 49ab6938f15b7871b826dd2bcf3d833f875113a3 Mon Sep 17 00:00:00 2001 From: Bob van de Vijver Date: Wed, 28 Aug 2024 11:03:59 +0200 Subject: [PATCH 2/2] Add battery percentage and tamper alarm to sensor devices when available --- lib/TuyaOAuth2DeviceSensor.ts | 18 ++++++++- lib/TuyaOAuth2DriverSensor.ts | 35 ++++++++++++++--- lib/migrations/MigrationStore.ts | 24 ++++++++++++ lib/migrations/TuyaSensorMigrations.ts | 54 ++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 lib/migrations/MigrationStore.ts create mode 100644 lib/migrations/TuyaSensorMigrations.ts diff --git a/lib/TuyaOAuth2DeviceSensor.ts b/lib/TuyaOAuth2DeviceSensor.ts index e8d8b58e..5e67a2b4 100644 --- a/lib/TuyaOAuth2DeviceSensor.ts +++ b/lib/TuyaOAuth2DeviceSensor.ts @@ -1,13 +1,29 @@ import { TuyaStatus } from '../types/TuyaTypes'; import TuyaOAuth2Device from './TuyaOAuth2Device'; +import * as TuyaSensorMigrations from '../lib/migrations/TuyaSensorMigrations'; export default class TuyaOAuth2DeviceSensor extends TuyaOAuth2Device { + async performMigrations(): Promise { + await super.performMigrations(); + await TuyaSensorMigrations.performMigrations(this); + } + async onTuyaStatus(status: TuyaStatus, changedStatusCodes: string[]): Promise { await super.onTuyaStatus(status, changedStatusCodes); // alarm_battery if (typeof status['battery_state'] === 'string') { - super.setCapabilityValue('alarm_battery', status['battery_state'] === 'low').catch(this.error); + await this.safeSetCapabilityValue('alarm_battery', status['battery_state'] === 'low'); + } + + // measure_battery + if (typeof status['battery_percentage'] === 'number') { + await this.safeSetCapabilityValue('measure_battery', status['battery_percentage']); + } + + // alarm_tamper + if (typeof status['temper_alarm'] === 'boolean') { + await this.safeSetCapabilityValue('alarm_tamper', status['temper_alarm']); } } } diff --git a/lib/TuyaOAuth2DriverSensor.ts b/lib/TuyaOAuth2DriverSensor.ts index 0eac036e..8c627c05 100644 --- a/lib/TuyaOAuth2DriverSensor.ts +++ b/lib/TuyaOAuth2DriverSensor.ts @@ -13,12 +13,35 @@ export default class TuyaOAuth2DriverSensor extends TuyaOAuth2Driver { ): ListDeviceProperties { const props = super.onTuyaPairListDeviceProperties(device, specifications, dataPoints); - // alarm_battery - const hasBatteryState = device.status.some(({ code }) => code === 'battery_state'); - if (hasBatteryState) { - props.store?.tuya_capabilities.push('battery_state'); - props.capabilities?.push('alarm_battery'); - } + const tuyaCodes = device.status.map(s => s.code); + const hasBatteryPercentage = tuyaCodes.includes('battery_percentage'); + + tuyaCodes.map(tuyaCode => { + switch (tuyaCode) { + case 'battery_state': + if (hasBatteryPercentage) { + // Do not add battery alarm if percentage is available + return; + } + + props.capabilities.push('alarm_battery'); + break; + + case 'battery_percentage': + props.capabilities.push('measure_battery'); + break; + + case 'temper_alarm': + props.capabilities.push('alarm_tamper'); + break; + + default: + // Default return to not add the capability + return; + } + + props.store.tuya_capabilities.push(tuyaCode); + }); return props; } diff --git a/lib/migrations/MigrationStore.ts b/lib/migrations/MigrationStore.ts new file mode 100644 index 00000000..a7faf5a7 --- /dev/null +++ b/lib/migrations/MigrationStore.ts @@ -0,0 +1,24 @@ +import type TuyaOAuth2Device from '../TuyaOAuth2Device'; + +const storeKey = '_migrations'; + +/** Execute a migration when not already done, based on the supplied migration id. */ +export async function executeMigration( + device: TuyaOAuth2Device, + migrationId: string, + migration: () => Promise, +): Promise { + let migrations: string[] | undefined = device.getStoreValue(storeKey); + if (!Array.isArray(migrations)) { + migrations = []; + } + + if (migrations.includes(migrationId)) { + return; + } + + await migration(); + + migrations.push(migrationId); + await device.setStoreValue(storeKey, migrations).catch(device.error); +} diff --git a/lib/migrations/TuyaSensorMigrations.ts b/lib/migrations/TuyaSensorMigrations.ts new file mode 100644 index 00000000..124dfc9f --- /dev/null +++ b/lib/migrations/TuyaSensorMigrations.ts @@ -0,0 +1,54 @@ +import TuyaOAuth2DeviceSensor from '../TuyaOAuth2DeviceSensor'; +import { executeMigration } from './MigrationStore'; + +export async function performMigrations(device: TuyaOAuth2DeviceSensor): Promise { + await addBatteryPercentageMigration(device).catch(device.error); + await addTemperAlarmMigration(device).catch(device.error); +} + +async function addBatteryPercentageMigration(device: TuyaOAuth2DeviceSensor): Promise { + await executeMigration(device, 'sensor_battery_percentage', async () => { + if (device.hasCapability('measure_battery') || device.hasCapability('alarm_battery')) { + // Don't touch existing devices that already have a battery related capability + device.log('Battery percentage migration skipped'); + + return; + } + + device.log('Migrating battery percentage...'); + + const status = await device.getStatus(); + const batteryPercentage = status.find(s => s.code === 'battery_percentage'); + if (!batteryPercentage) { + device.log('Battery percentage not supported'); + return; + } + + await device.addCapability('measure_battery'); + await device.safeSetCapabilityValue('measure_battery', batteryPercentage.value); + + device.log('Battery percentage added'); + }); +} + +async function addTemperAlarmMigration(device: TuyaOAuth2DeviceSensor): Promise { + await executeMigration(device, 'sensor_temper_alarm', async () => { + if (device.hasCapability('alarm_tamper')) { + return; + } + + device.log('Migrating tamper alarm...'); + + const status = await device.getStatus(); + const temperAlarm = status.find(s => s.code === 'temper_alarm'); + if (!temperAlarm) { + device.log('Tamper alarm not supported'); + return; + } + + await device.addCapability('alarm_tamper'); + await device.safeSetCapabilityValue('alarm_tamper', temperAlarm.value); + + device.log('Tamper alarm added'); + }); +}