diff --git a/.homeycompose/flow/actions/send_command_boolean.json b/.homeycompose/flow/actions/send_command_boolean.json index 1b1d1703..e3f3430e 100644 --- a/.homeycompose/flow/actions/send_command_boolean.json +++ b/.homeycompose/flow/actions/send_command_boolean.json @@ -13,7 +13,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" diff --git a/.homeycompose/flow/actions/send_command_json.json b/.homeycompose/flow/actions/send_command_json.json index 4e8b1e7c..d305c917 100644 --- a/.homeycompose/flow/actions/send_command_json.json +++ b/.homeycompose/flow/actions/send_command_json.json @@ -13,7 +13,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" diff --git a/.homeycompose/flow/actions/send_command_number.json b/.homeycompose/flow/actions/send_command_number.json index fa83c40f..25bcb7e1 100644 --- a/.homeycompose/flow/actions/send_command_number.json +++ b/.homeycompose/flow/actions/send_command_number.json @@ -13,7 +13,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" diff --git a/.homeycompose/flow/actions/send_command_string.json b/.homeycompose/flow/actions/send_command_string.json index c37e8884..0776b18a 100644 --- a/.homeycompose/flow/actions/send_command_string.json +++ b/.homeycompose/flow/actions/send_command_string.json @@ -13,7 +13,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" diff --git a/.homeycompose/flow/triggers/receive_status_boolean.json b/.homeycompose/flow/triggers/receive_status_boolean.json index 7d0007e4..f0f7ee75 100644 --- a/.homeycompose/flow/triggers/receive_status_boolean.json +++ b/.homeycompose/flow/triggers/receive_status_boolean.json @@ -13,7 +13,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" diff --git a/.homeycompose/flow/triggers/receive_status_json.json b/.homeycompose/flow/triggers/receive_status_json.json index d8f7be25..b0dd5e6a 100644 --- a/.homeycompose/flow/triggers/receive_status_json.json +++ b/.homeycompose/flow/triggers/receive_status_json.json @@ -13,7 +13,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" diff --git a/.homeycompose/flow/triggers/receive_status_number.json b/.homeycompose/flow/triggers/receive_status_number.json index 01714a69..f0330866 100644 --- a/.homeycompose/flow/triggers/receive_status_number.json +++ b/.homeycompose/flow/triggers/receive_status_number.json @@ -13,7 +13,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" diff --git a/.homeycompose/flow/triggers/receive_status_string.json b/.homeycompose/flow/triggers/receive_status_string.json index b6865d44..1e892ff7 100644 --- a/.homeycompose/flow/triggers/receive_status_string.json +++ b/.homeycompose/flow/triggers/receive_status_string.json @@ -13,7 +13,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" diff --git a/app.json b/app.json index 998f2ffd..eb09dfdc 100644 --- a/app.json +++ b/app.json @@ -400,7 +400,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" @@ -441,7 +441,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" @@ -482,7 +482,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" @@ -523,7 +523,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" @@ -920,7 +920,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" @@ -958,7 +958,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" @@ -996,7 +996,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" @@ -1034,7 +1034,7 @@ "name": "device", "type": "device", "filter": { - "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_contact|sensor_motion|sensor_smoke|siren|socket" + "driver_id": "camera|dimmer|doorbell|fan|heater|light|other|sensor_climate|sensor_contact|sensor_motion|sensor_smoke|siren|socket" }, "title": { "en": "Device" @@ -2924,6 +2924,206 @@ } ] }, + { + "capabilities": [ + "measure_temperature", + "measure_humidity" + ], + "connectivity": [ + "cloud" + ], + "platforms": [ + "local", + "cloud" + ], + "images": { + "small": "/drivers/sensor_climate/assets/images/small.png", + "large": "/drivers/sensor_climate/assets/images/large.png", + "xlarge": "/drivers/sensor_climate/assets/images/xlarge.png" + }, + "pair": [ + { + "id": "welcome", + "navigation": { + "next": "login_oauth2" + } + }, + { + "id": "login_oauth2", + "template": "login_oauth2" + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ], + "repair": [ + { + "id": "login_oauth2", + "template": "login_oauth2" + } + ], + "class": "sensor", + "name": { + "en": "Climate Sensor", + "nl": "Klimaatsensor" + }, + "id": "sensor_climate", + "settings": [ + { + "id": "va_temperature_scaling", + "type": "dropdown", + "label": { + "en": "Measured Temperature Scale" + }, + "hint": { + "en": "By how much the temperature measured by the device is scaled." + }, + "value": "1", + "values": [ + { + "id": "0", + "label": { + "en": "1" + } + }, + { + "id": "1", + "label": { + "en": "1/10" + } + }, + { + "id": "2", + "label": { + "en": "1/100" + } + }, + { + "id": "3", + "label": { + "en": "1/1000" + } + } + ] + }, + { + "id": "va_humidity_scaling", + "type": "dropdown", + "label": { + "en": "Measured Humidity Scale" + }, + "hint": { + "en": "By how much the humidity measured by the device is scaled." + }, + "value": "1", + "values": [ + { + "id": "0", + "label": { + "en": "1" + } + }, + { + "id": "1", + "label": { + "en": "1/10" + } + }, + { + "id": "2", + "label": { + "en": "1/100" + } + }, + { + "id": "3", + "label": { + "en": "1/1000" + } + } + ] + }, + { + "id": "bright_value_scaling", + "type": "dropdown", + "label": { + "en": "Measured Brightness Scale" + }, + "hint": { + "en": "By how much the brightness measured by the device is scaled." + }, + "value": "0", + "values": [ + { + "id": "0", + "label": { + "en": "1" + } + }, + { + "id": "1", + "label": { + "en": "1/10" + } + }, + { + "id": "2", + "label": { + "en": "1/100" + } + }, + { + "id": "3", + "label": { + "en": "1/1000" + } + } + ] + }, + { + "id": "use_alarm_timeout", + "type": "checkbox", + "hint": { + "en": "Turn the alarm off a set time after the start signal, even if no end signal is received." + }, + "label": { + "en": "Use Alarm Timeout" + }, + "value": false + }, + { + "id": "alarm_timeout", + "type": "number", + "label": { + "en": "Alarm Timeout" + }, + "value": 10, + "min": 1, + "units": { + "en": "seconds" + } + }, + { + "id": "deviceSpecification", + "type": "label", + "label": { + "en": "Device Specification" + }, + "hint": { + "en": "The Tuya specification of this device" + }, + "value": "" + } + ] + }, { "capabilities": [ "alarm_contact" diff --git a/drivers/sensor_climate/TuyaClimateSensorConstants.ts b/drivers/sensor_climate/TuyaClimateSensorConstants.ts new file mode 100644 index 00000000..7ed5f2d3 --- /dev/null +++ b/drivers/sensor_climate/TuyaClimateSensorConstants.ts @@ -0,0 +1,30 @@ +/** capabilities that map one to one to a Homey capability */ +export const CLIMATE_SENSOR_CAPABILITIES = { + read_write: ['switch'], + read_only: ['temper_alarm'], + read_only_scaled: ['va_temperature', 'va_humidity', 'bright_value'], + setting: [], +} as const; + +export const CLIMATE_CAPABILITY_MAPPING = { + switch: 'onoff', + va_temperature: 'measure_temperature', + va_humidity: 'measure_humidity', + bright_value: 'measure_luminance', + battery_value: 'measure_battery', + va_battery: 'measure_battery', +} as const; + +export const CLIMATE_SENSOR_SETTING_LABELS = { + va_temperature_scaling: 'Measured Temperature Scale', + va_humidity_scaling: 'Measured Humidity Scale', + bright_value_scaling: 'Measured Brightness Scale', +} as const; + +export type HomeyClimateSensorSettings = { + va_temperature_scaling: '0' | '1' | '2' | '3'; + va_humidity_scaling: '0' | '1' | '2' | '3'; + bright_value_scaling: '0' | '1' | '2' | '3'; +}; + +export type TuyaClimateSensorSettings = Record; diff --git a/drivers/sensor_climate/assets/icon.svg b/drivers/sensor_climate/assets/icon.svg new file mode 100644 index 00000000..ba6de1fb --- /dev/null +++ b/drivers/sensor_climate/assets/icon.svg @@ -0,0 +1,10 @@ + + + drivers/sensor_contact/assets/icon + + + + + + + diff --git a/drivers/sensor_climate/assets/images/large.png b/drivers/sensor_climate/assets/images/large.png new file mode 100644 index 00000000..096ea7c0 Binary files /dev/null and b/drivers/sensor_climate/assets/images/large.png differ diff --git a/drivers/sensor_climate/assets/images/small.png b/drivers/sensor_climate/assets/images/small.png new file mode 100644 index 00000000..d341241b Binary files /dev/null and b/drivers/sensor_climate/assets/images/small.png differ diff --git a/drivers/sensor_climate/assets/images/xlarge.png b/drivers/sensor_climate/assets/images/xlarge.png new file mode 100644 index 00000000..927c0537 Binary files /dev/null and b/drivers/sensor_climate/assets/images/xlarge.png differ diff --git a/drivers/sensor_climate/device.ts b/drivers/sensor_climate/device.ts new file mode 100644 index 00000000..f87d11e0 --- /dev/null +++ b/drivers/sensor_climate/device.ts @@ -0,0 +1,58 @@ +import { + HomeyClimateSensorSettings, + CLIMATE_CAPABILITY_MAPPING, + CLIMATE_SENSOR_CAPABILITIES, + CLIMATE_SENSOR_SETTING_LABELS, +} from './TuyaClimateSensorConstants'; +import TuyaOAuth2DeviceSensor from '../../lib/TuyaOAuth2DeviceSensor'; +import { constIncludes, getFromMap } from '../../lib/TuyaOAuth2Util'; +import { SettingsEvent, TuyaStatus } from '../../types/TuyaTypes'; +import * as TuyaOAuth2Util from '../../lib/TuyaOAuth2Util'; + +module.exports = class TuyaOAuth2DeviceSensorClimate extends TuyaOAuth2DeviceSensor { + async onOAuth2Init(): Promise { + await super.onOAuth2Init(); + + if (this.hasCapability('onoff')) { + this.registerCapabilityListener('onoff', value => this.sendCommand({ code: 'switch', value })); + } + } + + async onTuyaStatus(status: TuyaStatus, changedStatusCodes: string[]): Promise { + // battery_state, battery_percentage and temper_alarm are handled by the superclass + await super.onTuyaStatus(status, changedStatusCodes); + + for (const tuyaCapability in status) { + const homeyCapability = getFromMap(CLIMATE_CAPABILITY_MAPPING, tuyaCapability); + const value = status[tuyaCapability]; + + if ( + (constIncludes(CLIMATE_SENSOR_CAPABILITIES.read_only, tuyaCapability) || + constIncludes(CLIMATE_SENSOR_CAPABILITIES.read_write, tuyaCapability)) && + homeyCapability + ) { + await this.safeSetCapabilityValue(homeyCapability, value); + } + + if (constIncludes(CLIMATE_SENSOR_CAPABILITIES.read_only_scaled, tuyaCapability) && homeyCapability) { + const scaling = 10.0 ** Number.parseInt(this.getSetting(`${tuyaCapability}_scaling`) ?? '0', 10); + await this.safeSetCapabilityValue(homeyCapability, (status[tuyaCapability] as number) / scaling); + } + + // Battery + if (tuyaCapability === 'battery_value' && homeyCapability) { + const scaledValue = (value as number) / 300; + await this.safeSetCapabilityValue(homeyCapability, scaledValue); + } + + if (tuyaCapability === 'va_battery' && homeyCapability) { + const scaledValue = (value as number) / 100; + await this.safeSetCapabilityValue(homeyCapability, scaledValue); + } + } + } + + async onSettings(event: SettingsEvent): Promise { + return await TuyaOAuth2Util.onSettings(this, event, CLIMATE_SENSOR_SETTING_LABELS); + } +}; diff --git a/drivers/sensor_climate/driver.compose.json b/drivers/sensor_climate/driver.compose.json new file mode 100644 index 00000000..1f418312 --- /dev/null +++ b/drivers/sensor_climate/driver.compose.json @@ -0,0 +1,9 @@ +{ + "$extends": "tuya", + "class": "sensor", + "name": { + "en": "Climate Sensor", + "nl": "Klimaatsensor" + }, + "capabilities": ["measure_temperature", "measure_humidity"] +} diff --git a/drivers/sensor_climate/driver.settings.compose.json b/drivers/sensor_climate/driver.settings.compose.json new file mode 100644 index 00000000..89a6c03a --- /dev/null +++ b/drivers/sensor_climate/driver.settings.compose.json @@ -0,0 +1,122 @@ +[ + { + "id": "va_temperature_scaling", + "type": "dropdown", + "label": { + "en": "Measured Temperature Scale" + }, + "hint": { + "en": "By how much the temperature measured by the device is scaled." + }, + "value": "1", + "values": [ + { + "id": "0", + "label": { + "en": "1" + } + }, + { + "id": "1", + "label": { + "en": "1/10" + } + }, + { + "id": "2", + "label": { + "en": "1/100" + } + }, + { + "id": "3", + "label": { + "en": "1/1000" + } + } + ] + }, + { + "id": "va_humidity_scaling", + "type": "dropdown", + "label": { + "en": "Measured Humidity Scale" + }, + "hint": { + "en": "By how much the humidity measured by the device is scaled." + }, + "value": "1", + "values": [ + { + "id": "0", + "label": { + "en": "1" + } + }, + { + "id": "1", + "label": { + "en": "1/10" + } + }, + { + "id": "2", + "label": { + "en": "1/100" + } + }, + { + "id": "3", + "label": { + "en": "1/1000" + } + } + ] + }, + { + "id": "bright_value_scaling", + "type": "dropdown", + "label": { + "en": "Measured Brightness Scale" + }, + "hint": { + "en": "By how much the brightness measured by the device is scaled." + }, + "value": "0", + "values": [ + { + "id": "0", + "label": { + "en": "1" + } + }, + { + "id": "1", + "label": { + "en": "1/10" + } + }, + { + "id": "2", + "label": { + "en": "1/100" + } + }, + { + "id": "3", + "label": { + "en": "1/1000" + } + } + ] + }, + { + "$extends": "useAlarmTimeout" + }, + { + "$extends": "alarmTimeout" + }, + { + "$extends": "deviceSpecification" + } +] diff --git a/drivers/sensor_climate/driver.ts b/drivers/sensor_climate/driver.ts new file mode 100644 index 00000000..bdd35a20 --- /dev/null +++ b/drivers/sensor_climate/driver.ts @@ -0,0 +1,47 @@ +import { TuyaDeviceResponse, TuyaDeviceSpecificationResponse } from '../../types/TuyaApiTypes'; +import { CLIMATE_CAPABILITY_MAPPING, CLIMATE_SENSOR_CAPABILITIES } from './TuyaClimateSensorConstants'; +import { ListDeviceProperties } from '../../lib/TuyaOAuth2Driver'; +import TuyaOAuth2DriverSensor from '../../lib/TuyaOAuth2DriverSensor'; +import { DEVICE_CATEGORIES } from '../../lib/TuyaOAuth2Constants'; +import { constIncludes, getFromMap } from '../../lib/TuyaOAuth2Util'; + +module.exports = class TuyaOAuth2DriverSensorClimate extends TuyaOAuth2DriverSensor { + TUYA_DEVICE_CATEGORIES = [DEVICE_CATEGORIES.SECURITY_VIDEO_SURV.TEMP_HUMI_SENSOR]; + + onTuyaPairListDeviceProperties( + device: TuyaDeviceResponse, + specifications: TuyaDeviceSpecificationResponse, + ): ListDeviceProperties { + // battery_state, battery_percentage and temper_alarm are handled by the superclass + const props = super.onTuyaPairListDeviceProperties(device, specifications); + + for (const status of device.status) { + const tuyaCapability = status.code; + const homeyCapability = getFromMap(CLIMATE_CAPABILITY_MAPPING, tuyaCapability); + + // Capabilities that map one to one + if (homeyCapability) { + props.store.tuya_capabilities.push(tuyaCapability); + props.capabilities.push(homeyCapability); + } + } + + // Remove duplicate capabilities + props.capabilities = [...new Set(props.capabilities)]; + + for (const statusSpecifications of specifications.status) { + const tuyaCapability = statusSpecifications.code; + const values = JSON.parse(statusSpecifications.values); + + if (constIncludes(CLIMATE_SENSOR_CAPABILITIES.read_only_scaled, tuyaCapability)) { + if ([0, 1, 2, 3].includes(values.scale)) { + props.settings[`${tuyaCapability}_scaling`] = `${values.scale}`; + } else { + this.error(`Unsupported ${tuyaCapability} scale:`, values.scale); + } + } + } + + return props; + } +}; diff --git a/drivers/sensor_climate/pair/welcome.assets/chevron-right.png b/drivers/sensor_climate/pair/welcome.assets/chevron-right.png new file mode 100644 index 00000000..d7331b23 Binary files /dev/null and b/drivers/sensor_climate/pair/welcome.assets/chevron-right.png differ diff --git a/drivers/sensor_climate/pair/welcome.assets/logos.png b/drivers/sensor_climate/pair/welcome.assets/logos.png new file mode 100644 index 00000000..faefa496 Binary files /dev/null and b/drivers/sensor_climate/pair/welcome.assets/logos.png differ diff --git a/drivers/sensor_climate/pair/welcome.html b/drivers/sensor_climate/pair/welcome.html new file mode 100644 index 00000000..14a7e06e --- /dev/null +++ b/drivers/sensor_climate/pair/welcome.html @@ -0,0 +1,60 @@ + + +

+ +

+ +

+ +

+ + + + +

+ +

+ +