From 650116708a170519eb902072aa0c6057692a6aa8 Mon Sep 17 00:00:00 2001 From: Francois Blackburn Date: Sun, 4 Jun 2023 10:04:46 -0400 Subject: [PATCH 01/11] RM3500ZB: add first version --- drivers/Calypso-Sinope-RM3500ZB.groovy | 216 +++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 drivers/Calypso-Sinope-RM3500ZB.groovy diff --git a/drivers/Calypso-Sinope-RM3500ZB.groovy b/drivers/Calypso-Sinope-RM3500ZB.groovy new file mode 100644 index 0000000..a2f5ae5 --- /dev/null +++ b/drivers/Calypso-Sinope-RM3500ZB.groovy @@ -0,0 +1,216 @@ +/* groovylint-disable DuplicateNumberLiteral */ +/* groovylint-disable UnnecessaryGetter */ +/** + * Sinope Zigbee Plug (RM3500ZB) Device Driver for Hubitat + * + * 1.0 (2023-05-07): Initial release + * Author: fblackburn + * Inspired by: + * - Sinope => https://github.com/SmartThingsCommunity/SmartThingsPublic/tree/master/devicetypes/sinope-technologies + * - sacua => https://github.com/sacua/SinopeDriverHubitat/blob/main/drivers/RM3500ZB_Sinope_Hubitat.groovy + */ + +import hubitat.device.HubMultiAction + +metadata +{ + definition( + name: 'Sinope Zigbee Calypso (RM3500ZB)', + namespace: 'fblackburn', + author: 'fblackburn', + ) { + capability 'Switch' + capability 'Configuration' + capability 'Refresh' + capability 'Outlet' + capability 'PowerMeter' + capability 'EnergyMeter' + } + preferences { + input( + name: 'powerChange', + type: 'number', + title: 'Power Change', + description: ''' + |Difference of watts to trigger power report (1..10000). + |Power below this value will be considered as 0 + |'''.stripMargin(), + range: '1..10000', + defaultValue: getDefaultPowerChange() + ) + input( + name: 'trace', + type: 'bool', + title: 'Enable debug logging', + defaultValue: false + ) + } +} + +void installed() { + if (settings.trace) { + log.trace 'RM3500ZB >> installed()' + } + configure() + refresh() +} + +void updated() { + if (settings.trace) { + log.trace 'RM3500ZB >> updated()' + } + configure() + refresh() +} + +void uninstalled() { + if (settings.trace) { + log.trace 'RM3500ZB >> uninstalled()' + } + unschedule() +} + +void configure() { + if (settings.trace) { + log.trace 'RM3500ZB >> configure()' + } + try { + unschedule() + } catch (ignored) { } + + List cmds = [] + cmds += zigbee.configureReporting(0x0006, 0x0000, DataType.BOOLEAN, 0, 3600, null) // State + Integer powerChange = settings.powerChange == null ? getDefaultPowerChange() : settings.powerChange + cmds += zigbee.configureReporting(0x0B04, 0x050B, DataType.INT16, 0, 600, powerChange) // Power + // Logic made by device to trigger an event: + // if energyChange <= energyValue; then trigger event + // Since energyValue will only increase with time, we can only use minReportTime (300) + cmds += zigbee.configureReporting(0x0702, 0x0000, DataType.UINT48, 300, 1800, 0) // Energy + sendCommands(cmds) +} + +Map parse(String description) { + if (!description?.startsWith('read attr -')) { + if (!description?.startsWith('catchall:')) { + log.warn "RM3500ZB >> parse(description) ==> Unhandled event: ${description}" + } + return [:] + } + + Map event = [:] + Map descMap = zigbee.parseDescriptionAsMap(description) + if (descMap.cluster == '0006' && descMap.attrId == '0000') { + event.name = 'switch' + event.value = getSwitchMap()[descMap.value] + } else if (descMap.cluster == '0B04' && descMap.attrId == '050B') { + Double power = getPower(descMap.value) + Double oldPower = device.currentValue('power') + if (power != 0.0 && power < oldPower) { // check if power decrease + Integer powerChange = settings.powerChange == null ? getDefaultPowerChange() : settings.powerChange + if (power < powerChange) { + // No other even will be sent if power is lower than powerChange + // So we will prioritze to send 0 value instead of the "real" value + if (settings.trace) { + log.trace "RM3500ZB >> power(${power}) hardcoded to 0" + } + power = 0.0 + } + } + event.name = 'power' + event.value = power + event.unit = 'W' + } else if (descMap.cluster == '0702' && descMap.attrId == '0000') { + BigInteger newEnergyValue = getEnergy(descMap.value) + if (newEnergyValue == 0) { + log.info 'RM3500ZB >> Ignoring energy event (Caused: power outage or new pairing device)' + } else { + event.name = 'energy' + event.value = newEnergyValue / 1000 + event.unit = 'kWh' + } + } else { + log.warn "RM3500ZB >> parse(descMap) ==> Unhandled attribute: ${descMap}" + } + + if (event.name != null && event.value != null) { + event.descriptionText = "${device.getLabel()} ${event.name} is ${event.value}" + if (event.unit) { + event.descriptionText = "${event.descriptionText}${event.unit}" + } + } + + if (settings.trace) { + log.trace "RM3500ZB >> parse(description) ==> ${event.name}: ${event.value}" + } + if (descMap.additionalAttrs) { + if (settings.trace) { + log.warn "RM3500ZB >> Found additionalAttrs: ${descMap}" + } + } + return event +} + +void refresh() { + if (settings.trace) { + log.trace 'RM3500ZB >> refresh()' + } + if (state.updatedLastRanAt && now() < state.updatedLastRanAt + 2000) { + if (settings.trace) { + log.trace 'RM3500ZB >> refresh() ==> Ran within last 2 seconds so aborting' + } + return + } + state.updatedLastRanAt = now() + + List cmds = [] + cmds += zigbee.readAttribute(0x0006, 0x0000) // State + cmds += zigbee.readAttribute(0x0B04, 0x050B) // Active power + cmds += zigbee.readAttribute(0x0702, 0x0000) // Energy delivered + sendCommands(cmds) +} + +void off() { + if (settings.trace) { + log.trace 'RM3500ZB >> off()' + } + List cmds = zigbee.command(0x0006, 0x00) // Off + sendCommands(cmds) +} + +void on() { + if (settings.trace) { + log.trace 'RM3500ZB >> on()' + } + List cmds = zigbee.command(0x0006, 0x01) // On + sendCommands(cmds) +} + +private void sendCommands(List commands) { + HubMultiAction actions = new HubMultiAction(commands, hubitat.device.Protocol.ZIGBEE) + sendHubCommand(actions) +} + +private Map getSwitchMap() { + return [ + '00': 'off', + '01': 'on', + ] +} + +private Double getPower(String value) { + if (value == null) { + return + } + return Integer.parseInt(value, 16) / 10 +} + +private BigInteger getEnergy(String value) { + if (value == null) { + return 0 + } + return new BigInteger(value, 16) +} + +private Integer getDefaultPowerChange() { + return 10 +} From 640773f0aca56f9c29004ff57e0c973645dc0546 Mon Sep 17 00:00:00 2001 From: Francois Blackburn Date: Sun, 4 Jun 2023 10:42:57 -0400 Subject: [PATCH 02/11] RM3500ZB: add temperature measurement --- drivers/Calypso-Sinope-RM3500ZB.groovy | 31 +++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/drivers/Calypso-Sinope-RM3500ZB.groovy b/drivers/Calypso-Sinope-RM3500ZB.groovy index a2f5ae5..bab35e5 100644 --- a/drivers/Calypso-Sinope-RM3500ZB.groovy +++ b/drivers/Calypso-Sinope-RM3500ZB.groovy @@ -25,6 +25,7 @@ metadata capability 'Outlet' capability 'PowerMeter' capability 'EnergyMeter' + capability 'TemperatureMeasurement' } preferences { input( @@ -79,13 +80,18 @@ void configure() { } catch (ignored) { } List cmds = [] - cmds += zigbee.configureReporting(0x0006, 0x0000, DataType.BOOLEAN, 0, 3600, null) // State + cmds += zigbee.configureReporting(0x0006, 0x0000, DataType.BOOLEAN, 0, 3600, null) // State + + Integer temperatureChange = 50 // 0.5 celcius + cmds += zigbee.configureReporting(0x0402, 0x0000, DataType.INT16, 30, 580, temperatureChange) // Temperature sensor + Integer powerChange = settings.powerChange == null ? getDefaultPowerChange() : settings.powerChange - cmds += zigbee.configureReporting(0x0B04, 0x050B, DataType.INT16, 0, 600, powerChange) // Power + cmds += zigbee.configureReporting(0x0B04, 0x050B, DataType.INT16, 0, 600, powerChange) // Power + // Logic made by device to trigger an event: // if energyChange <= energyValue; then trigger event // Since energyValue will only increase with time, we can only use minReportTime (300) - cmds += zigbee.configureReporting(0x0702, 0x0000, DataType.UINT48, 300, 1800, 0) // Energy + cmds += zigbee.configureReporting(0x0702, 0x0000, DataType.UINT48, 300, 1800, 0) // Energy sendCommands(cmds) } @@ -128,6 +134,11 @@ Map parse(String description) { event.value = newEnergyValue / 1000 event.unit = 'kWh' } + } else if (descMap.cluster == '0402' && descMap.attrId == '0000') { + String scale = getTemperatureScale() + event.name = 'temperature' + event.value = getTemperatureValue(descMap.value, scale) + event.unit = "°${scale}" } else { log.warn "RM3500ZB >> parse(descMap) ==> Unhandled attribute: ${descMap}" } @@ -166,6 +177,7 @@ void refresh() { cmds += zigbee.readAttribute(0x0006, 0x0000) // State cmds += zigbee.readAttribute(0x0B04, 0x050B) // Active power cmds += zigbee.readAttribute(0x0702, 0x0000) // Energy delivered + cmds += zigbee.readAttribute(0x0402, 0x0000) // Temperature sensor sendCommands(cmds) } @@ -211,6 +223,19 @@ private BigInteger getEnergy(String value) { return new BigInteger(value, 16) } +private Double getTemperatureValue(String value, String scale) { + // 8000 is the value when sensor is not plugged + if (value == '8000' || value == null) { + return + } + + Double celsius = (Integer.parseInt(value, 16) / 100).toDouble() + if (scale == 'C') { + return celsius.round(1) + } + return Math.round(celsiusToFahrenheit(celsius)) +} + private Integer getDefaultPowerChange() { return 10 } From 16fc99177f830eb3ac5efc6a0610fd4e43b91ea2 Mon Sep 17 00:00:00 2001 From: Francois Blackburn Date: Sun, 4 Jun 2023 11:11:49 -0400 Subject: [PATCH 03/11] RM3500ZB: add RMS voltage measurement --- drivers/Calypso-Sinope-RM3500ZB.groovy | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/drivers/Calypso-Sinope-RM3500ZB.groovy b/drivers/Calypso-Sinope-RM3500ZB.groovy index bab35e5..b351bd3 100644 --- a/drivers/Calypso-Sinope-RM3500ZB.groovy +++ b/drivers/Calypso-Sinope-RM3500ZB.groovy @@ -26,6 +26,7 @@ metadata capability 'PowerMeter' capability 'EnergyMeter' capability 'TemperatureMeasurement' + capability 'VoltageMeasurement' } preferences { input( @@ -125,6 +126,11 @@ Map parse(String description) { event.name = 'power' event.value = power event.unit = 'W' + } else if (descMap.cluster == '0B04' && descMap.attrId == '0505') { + // This event seems to be triggered at same frequency than Power + event.name = 'voltage' + event.value = getVoltage(descMap.value) + event.unit = 'V' } else if (descMap.cluster == '0702' && descMap.attrId == '0000') { BigInteger newEnergyValue = getEnergy(descMap.value) if (newEnergyValue == 0) { @@ -176,6 +182,7 @@ void refresh() { List cmds = [] cmds += zigbee.readAttribute(0x0006, 0x0000) // State cmds += zigbee.readAttribute(0x0B04, 0x050B) // Active power + cmds += zigbee.readAttribute(0x0B04, 0x0505) // Voltage cmds += zigbee.readAttribute(0x0702, 0x0000) // Energy delivered cmds += zigbee.readAttribute(0x0402, 0x0000) // Temperature sensor sendCommands(cmds) @@ -223,6 +230,13 @@ private BigInteger getEnergy(String value) { return new BigInteger(value, 16) } +private Integer getVoltage(String value) { + if (value == null) { + return 0 + } + return Integer.parseInt(value, 16) +} + private Double getTemperatureValue(String value, String scale) { // 8000 is the value when sensor is not plugged if (value == '8000' || value == null) { From bb6513efaf33aebda7daa9b69a64cae0e68fa5c2 Mon Sep 17 00:00:00 2001 From: Francois Blackburn Date: Sun, 4 Jun 2023 11:26:10 -0400 Subject: [PATCH 04/11] RM3500ZB: extract parser to read additionalAttrs --- drivers/Calypso-Sinope-RM3500ZB.groovy | 102 ++++++++++++++----------- 1 file changed, 57 insertions(+), 45 deletions(-) diff --git a/drivers/Calypso-Sinope-RM3500ZB.groovy b/drivers/Calypso-Sinope-RM3500ZB.groovy index b351bd3..35e7aba 100644 --- a/drivers/Calypso-Sinope-RM3500ZB.groovy +++ b/drivers/Calypso-Sinope-RM3500ZB.groovy @@ -96,16 +96,70 @@ void configure() { sendCommands(cmds) } -Map parse(String description) { +List parse(String description) { if (!description?.startsWith('read attr -')) { if (!description?.startsWith('catchall:')) { log.warn "RM3500ZB >> parse(description) ==> Unhandled event: ${description}" } - return [:] + return [] } - Map event = [:] Map descMap = zigbee.parseDescriptionAsMap(description) + Map event = extractEvent(descMap) + List events = [event] + if (descMap.additionalAttrs) { + // When many events from same cluster must be sent at the same time, + // device send other events in additionalAttrs instead of sending several + if (settings.trace) { + log.trace "TH112XZB >> Found additionalAttrs: ${descMap}" + } + descMap.additionalAttrs.each { Map attribute -> + attribute.cluster = descMap.cluster + events.add(extractEvent(attribute)) + } + } + return events +} + +void refresh() { + if (settings.trace) { + log.trace 'RM3500ZB >> refresh()' + } + if (state.updatedLastRanAt && now() < state.updatedLastRanAt + 2000) { + if (settings.trace) { + log.trace 'RM3500ZB >> refresh() ==> Ran within last 2 seconds so aborting' + } + return + } + state.updatedLastRanAt = now() + + List cmds = [] + cmds += zigbee.readAttribute(0x0006, 0x0000) // State + cmds += zigbee.readAttribute(0x0B04, 0x050B) // Active power + cmds += zigbee.readAttribute(0x0B04, 0x0505) // Voltage + cmds += zigbee.readAttribute(0x0702, 0x0000) // Energy delivered + cmds += zigbee.readAttribute(0x0402, 0x0000) // Temperature sensor + sendCommands(cmds) +} + +void off() { + if (settings.trace) { + log.trace 'RM3500ZB >> off()' + } + List cmds = zigbee.command(0x0006, 0x00) // Off + sendCommands(cmds) +} + +void on() { + if (settings.trace) { + log.trace 'RM3500ZB >> on()' + } + List cmds = zigbee.command(0x0006, 0x01) // On + sendCommands(cmds) +} + +private Map extractEvent(Map descMap) { + Map event = [:] if (descMap.cluster == '0006' && descMap.attrId == '0000') { event.name = 'switch' event.value = getSwitchMap()[descMap.value] @@ -159,51 +213,9 @@ Map parse(String description) { if (settings.trace) { log.trace "RM3500ZB >> parse(description) ==> ${event.name}: ${event.value}" } - if (descMap.additionalAttrs) { - if (settings.trace) { - log.warn "RM3500ZB >> Found additionalAttrs: ${descMap}" - } - } return event } -void refresh() { - if (settings.trace) { - log.trace 'RM3500ZB >> refresh()' - } - if (state.updatedLastRanAt && now() < state.updatedLastRanAt + 2000) { - if (settings.trace) { - log.trace 'RM3500ZB >> refresh() ==> Ran within last 2 seconds so aborting' - } - return - } - state.updatedLastRanAt = now() - - List cmds = [] - cmds += zigbee.readAttribute(0x0006, 0x0000) // State - cmds += zigbee.readAttribute(0x0B04, 0x050B) // Active power - cmds += zigbee.readAttribute(0x0B04, 0x0505) // Voltage - cmds += zigbee.readAttribute(0x0702, 0x0000) // Energy delivered - cmds += zigbee.readAttribute(0x0402, 0x0000) // Temperature sensor - sendCommands(cmds) -} - -void off() { - if (settings.trace) { - log.trace 'RM3500ZB >> off()' - } - List cmds = zigbee.command(0x0006, 0x00) // Off - sendCommands(cmds) -} - -void on() { - if (settings.trace) { - log.trace 'RM3500ZB >> on()' - } - List cmds = zigbee.command(0x0006, 0x01) // On - sendCommands(cmds) -} - private void sendCommands(List commands) { HubMultiAction actions = new HubMultiAction(commands, hubitat.device.Protocol.ZIGBEE) sendHubCommand(actions) From 620fde96a9758dd837c73ec9fa286031a268a4e7 Mon Sep 17 00:00:00 2001 From: Francois Blackburn Date: Sun, 4 Jun 2023 11:32:30 -0400 Subject: [PATCH 05/11] RM3500ZB: add amperage measurement --- drivers/Calypso-Sinope-RM3500ZB.groovy | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/drivers/Calypso-Sinope-RM3500ZB.groovy b/drivers/Calypso-Sinope-RM3500ZB.groovy index 35e7aba..83dad9b 100644 --- a/drivers/Calypso-Sinope-RM3500ZB.groovy +++ b/drivers/Calypso-Sinope-RM3500ZB.groovy @@ -26,6 +26,7 @@ metadata capability 'PowerMeter' capability 'EnergyMeter' capability 'TemperatureMeasurement' + capability 'CurrentMeter' capability 'VoltageMeasurement' } preferences { @@ -137,6 +138,7 @@ void refresh() { cmds += zigbee.readAttribute(0x0006, 0x0000) // State cmds += zigbee.readAttribute(0x0B04, 0x050B) // Active power cmds += zigbee.readAttribute(0x0B04, 0x0505) // Voltage + cmds += zigbee.readAttribute(0x0B04, 0x0508) // Amperage cmds += zigbee.readAttribute(0x0702, 0x0000) // Energy delivered cmds += zigbee.readAttribute(0x0402, 0x0000) // Temperature sensor sendCommands(cmds) @@ -185,6 +187,11 @@ private Map extractEvent(Map descMap) { event.name = 'voltage' event.value = getVoltage(descMap.value) event.unit = 'V' + } else if (descMap.cluster == '0B04' && descMap.attrId == '0508') { + // This event is sent with voltage event + event.name = 'amperage' + event.value = getAmperage(descMap.value) + event.unit = 'A' } else if (descMap.cluster == '0702' && descMap.attrId == '0000') { BigInteger newEnergyValue = getEnergy(descMap.value) if (newEnergyValue == 0) { @@ -249,6 +256,13 @@ private Integer getVoltage(String value) { return Integer.parseInt(value, 16) } +private Double getAmperage(String value) { + if (value == null) { + return 0 + } + return Integer.parseInt(value, 16) / 1000 +} + private Double getTemperatureValue(String value, String scale) { // 8000 is the value when sensor is not plugged if (value == '8000' || value == null) { From 46fe3b93658820acbe606bdec8ec5123dea205d9 Mon Sep 17 00:00:00 2001 From: Francois Blackburn Date: Sun, 4 Jun 2023 12:03:17 -0400 Subject: [PATCH 06/11] RM3500ZB: ignore ias zone events --- drivers/Calypso-Sinope-RM3500ZB.groovy | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/Calypso-Sinope-RM3500ZB.groovy b/drivers/Calypso-Sinope-RM3500ZB.groovy index 83dad9b..dec9a0c 100644 --- a/drivers/Calypso-Sinope-RM3500ZB.groovy +++ b/drivers/Calypso-Sinope-RM3500ZB.groovy @@ -99,7 +99,9 @@ void configure() { List parse(String description) { if (!description?.startsWith('read attr -')) { - if (!description?.startsWith('catchall:')) { + if (description?.startsWith('zone')) { + log.trace "RM3500ZB >> parse(description) Ingoring zone event: ${description}" + } else if (!description?.startsWith('catchall:')) { log.warn "RM3500ZB >> parse(description) ==> Unhandled event: ${description}" } return [] From c4eb3d78dedd6ae9a4d2d11b472721d1c5f3ac83 Mon Sep 17 00:00:00 2001 From: Francois Blackburn Date: Mon, 25 Nov 2024 20:04:37 -0500 Subject: [PATCH 07/11] RM3500ZB: fix getPower conversion --- drivers/Calypso-Sinope-RM3500ZB.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/Calypso-Sinope-RM3500ZB.groovy b/drivers/Calypso-Sinope-RM3500ZB.groovy index dec9a0c..1f83c30 100644 --- a/drivers/Calypso-Sinope-RM3500ZB.groovy +++ b/drivers/Calypso-Sinope-RM3500ZB.groovy @@ -1,4 +1,4 @@ -/* groovylint-disable DuplicateNumberLiteral */ +/* groovylint-disable DuplicateMapLiteral, DuplicateNumberLiteral */ /* groovylint-disable UnnecessaryGetter */ /** * Sinope Zigbee Plug (RM3500ZB) Device Driver for Hubitat @@ -241,7 +241,7 @@ private Double getPower(String value) { if (value == null) { return } - return Integer.parseInt(value, 16) / 10 + return Integer.parseInt(value, 16) } private BigInteger getEnergy(String value) { From 23040032d67768546f64197413dfdae147ad2bee Mon Sep 17 00:00:00 2001 From: Francois Blackburn Date: Mon, 25 Nov 2024 20:32:11 -0500 Subject: [PATCH 08/11] RM3500ZB: add safetyWaterTemperature feature --- drivers/Calypso-Sinope-RM3500ZB.groovy | 71 +++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/drivers/Calypso-Sinope-RM3500ZB.groovy b/drivers/Calypso-Sinope-RM3500ZB.groovy index 1f83c30..57c0440 100644 --- a/drivers/Calypso-Sinope-RM3500ZB.groovy +++ b/drivers/Calypso-Sinope-RM3500ZB.groovy @@ -28,6 +28,9 @@ metadata capability 'TemperatureMeasurement' capability 'CurrentMeter' capability 'VoltageMeasurement' + + command('enableSafetyWaterTemperature') + command('disableSafetyWaterTemperature') } preferences { input( @@ -41,6 +44,14 @@ metadata range: '1..10000', defaultValue: getDefaultPowerChange() ) + input( + name: 'safetyWaterTemperature', + type: 'number', + title: 'Set safety minimum water heater temperature (0 to disable)', + // According sinope: it should be configurable from 45 to 55 (and off) + range: '0..100', + defaultValue: getDefaultSafetyWaterTemperature() + ) input( name: 'trace', type: 'bool', @@ -88,12 +99,23 @@ void configure() { cmds += zigbee.configureReporting(0x0402, 0x0000, DataType.INT16, 30, 580, temperatureChange) // Temperature sensor Integer powerChange = settings.powerChange == null ? getDefaultPowerChange() : settings.powerChange - cmds += zigbee.configureReporting(0x0B04, 0x050B, DataType.INT16, 0, 600, powerChange) // Power + cmds += zigbee.configureReporting(0x0B04, 0x050B, DataType.INT16, 0, 600, powerChange) // Power // Logic made by device to trigger an event: // if energyChange <= energyValue; then trigger event // Since energyValue will only increase with time, we can only use minReportTime (300) cmds += zigbee.configureReporting(0x0702, 0x0000, DataType.UINT48, 300, 1800, 0) // Energy + + // Safety water temperature + Map params = [mfgCode: '0x119C'] + Integer frequency = 24 * 60 * 60 // 24 hours + cmds += zigbee.configureReporting(0xFF01, 0x0076, DataType.UINT8, 0, frequency, null, params) + if (settings.safetyWaterTemperature == 0) { + disableSafetyWaterTemperature() + } else { + enableSafetyWaterTemperature() + } + sendCommands(cmds) } @@ -143,6 +165,8 @@ void refresh() { cmds += zigbee.readAttribute(0x0B04, 0x0508) // Amperage cmds += zigbee.readAttribute(0x0702, 0x0000) // Energy delivered cmds += zigbee.readAttribute(0x0402, 0x0000) // Temperature sensor + cmds += zigbee.readAttribute(0xFF01, 0x0076, [mfgCode: '0x119C']) // Water heater temperature safety + sendCommands(cmds) } @@ -162,6 +186,23 @@ void on() { sendCommands(cmds) } +void enableSafetyWaterTemperature() { + if (settings.trace) { + log.trace 'RM3500ZB >> enableSafetyWaterTemperature()' + } + Integer safetyValue = settings.safetyWaterTemperature + List cmds = zigbee.writeAttribute(0xFF01, 0x0076, DataType.UINT8, safetyValue, [mfgCode: '0x119C']) + sendCommands(cmds) +} + +void disableSafetyWaterTemperature() { + if (settings.trace) { + log.trace 'RM3500ZB >> disableSafetyWaterTemperature()' + } + List cmds = zigbee.writeAttribute(0xFF01, 0x0076, DataType.UINT8, 0, [mfgCode: '0x119C']) + sendCommands(cmds) +} + private Map extractEvent(Map descMap) { Map event = [:] if (descMap.cluster == '0006' && descMap.attrId == '0000') { @@ -208,6 +249,26 @@ private Map extractEvent(Map descMap) { event.name = 'temperature' event.value = getTemperatureValue(descMap.value, scale) event.unit = "°${scale}" + } else if (descMap.cluster == 'FF01') { + switch (descMap.attrId) { + case '0076': + event.name = 'safetyWaterTemperature' + event.value = getSafetyWaterTemperature(descMap.value) + break + + case '0070': + case '0200': + case '0283': + if (settings.trace) { + log.trace "RM3500ZB >> parse(descMap) ==> Ignored attribute: ${descMap}" + } + break + + default: + log.warn "RM3500ZB >> parse(descMap) ==> Unhandled attribute: ${descMap}" + break + + } } else { log.warn "RM3500ZB >> parse(descMap) ==> Unhandled attribute: ${descMap}" } @@ -278,6 +339,14 @@ private Double getTemperatureValue(String value, String scale) { return Math.round(celsiusToFahrenheit(celsius)) } +private Integer getSafetyWaterTemperature(String value) { + return Integer.parseInt(value, 16) +} + private Integer getDefaultPowerChange() { return 10 } + +private Integer getDefaultSafetyWaterTemperature() { + return 45 +} \ No newline at end of file From dbe95919aa6d3644622cb87b58f3032917fcc75c Mon Sep 17 00:00:00 2001 From: Francois Blackburn Date: Mon, 25 Nov 2024 20:32:46 -0500 Subject: [PATCH 09/11] RM3500ZB: add water leak sensor --- drivers/Calypso-Sinope-RM3500ZB.groovy | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/drivers/Calypso-Sinope-RM3500ZB.groovy b/drivers/Calypso-Sinope-RM3500ZB.groovy index 57c0440..277e8d7 100644 --- a/drivers/Calypso-Sinope-RM3500ZB.groovy +++ b/drivers/Calypso-Sinope-RM3500ZB.groovy @@ -28,6 +28,7 @@ metadata capability 'TemperatureMeasurement' capability 'CurrentMeter' capability 'VoltageMeasurement' + capability "WaterSensor" command('enableSafetyWaterTemperature') command('disableSafetyWaterTemperature') @@ -116,6 +117,8 @@ void configure() { enableSafetyWaterTemperature() } + // Water leak sensor state + cmds += zigbee.configureReporting(0x0500, 0x0002, DataType.BITMAP16, 0, frequency, 0) sendCommands(cmds) } @@ -166,6 +169,7 @@ void refresh() { cmds += zigbee.readAttribute(0x0702, 0x0000) // Energy delivered cmds += zigbee.readAttribute(0x0402, 0x0000) // Temperature sensor cmds += zigbee.readAttribute(0xFF01, 0x0076, [mfgCode: '0x119C']) // Water heater temperature safety + cmds += zigbee.readAttribute(0x0500, 0x0002) // Read Water leak sendCommands(cmds) } @@ -249,6 +253,9 @@ private Map extractEvent(Map descMap) { event.name = 'temperature' event.value = getTemperatureValue(descMap.value, scale) event.unit = "°${scale}" + } else if (descMap.cluster == '0500' && descMap.attrId == '0002') { + event.name = 'water' + event.value = getWaterSensorValue(descMap.value) } else if (descMap.cluster == 'FF01') { switch (descMap.attrId) { case '0076': @@ -339,6 +346,15 @@ private Double getTemperatureValue(String value, String scale) { return Math.round(celsiusToFahrenheit(celsius)) } +private String getWaterSensorValue(String value) { + if (value == '0030') { + return 'dry' + } + if (value == '0031') { + return 'wet' + } +} + private Integer getSafetyWaterTemperature(String value) { return Integer.parseInt(value, 16) } From e589a53525a01bd319da293863990e99396f87d9 Mon Sep 17 00:00:00 2001 From: Francois Blackburn Date: Mon, 25 Nov 2024 20:37:53 -0500 Subject: [PATCH 10/11] fix styling issues --- drivers/Calypso-Sinope-RM3500ZB.groovy | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/drivers/Calypso-Sinope-RM3500ZB.groovy b/drivers/Calypso-Sinope-RM3500ZB.groovy index 277e8d7..bb9ec52 100644 --- a/drivers/Calypso-Sinope-RM3500ZB.groovy +++ b/drivers/Calypso-Sinope-RM3500ZB.groovy @@ -28,7 +28,7 @@ metadata capability 'TemperatureMeasurement' capability 'CurrentMeter' capability 'VoltageMeasurement' - capability "WaterSensor" + capability 'WaterSensor' command('enableSafetyWaterTemperature') command('disableSafetyWaterTemperature') @@ -274,7 +274,6 @@ private Map extractEvent(Map descMap) { default: log.warn "RM3500ZB >> parse(descMap) ==> Unhandled attribute: ${descMap}" break - } } else { log.warn "RM3500ZB >> parse(descMap) ==> Unhandled attribute: ${descMap}" @@ -365,4 +364,4 @@ private Integer getDefaultPowerChange() { private Integer getDefaultSafetyWaterTemperature() { return 45 -} \ No newline at end of file +} From c4137ed9e91577fc926b07be43da580eba567e66 Mon Sep 17 00:00:00 2001 From: Francois Blackburn Date: Mon, 25 Nov 2024 20:40:30 -0500 Subject: [PATCH 11/11] github: update node-version --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b9b0a3e..9dd437e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: - node-version: [ 16.x ] + node-version: [ 22.x ] steps: - uses: actions/checkout@v2