diff --git a/drivers/SmartThings/zigbee-switch/profiles/switch-power-waterheater.yml b/drivers/SmartThings/zigbee-switch/profiles/switch-power-waterheater.yml index 327a3dca2b..56acb58b47 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/switch-power-waterheater.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/switch-power-waterheater.yml @@ -6,21 +6,77 @@ components: version: 1 - id: powerMeter version: 1 - - id: voltageMeasurement - version: 1 - # - id: currentMeter - # version: 1 - - id: energyMeter - version: 1 - id: temperatureMeasurement version: 1 - id: waterSensor version: 1 - - id: thermostatCoolingSetpoint + - id: voltageMeasurement + version: 1 + - id: currentMeasurement + version: 1 + - id: energyMeter version: 1 - id: firmwareUpdate version: 1 - id: refresh version: 1 categories: - - name: SmartPlug \ No newline at end of file + - name: SmartPlug + +preferences: + - name: "lowTempProtection" + title: "Low Water Temperature Protection" + description: "Minimum temperature threshold to restart water heater" + required: false + preferenceType: enumeration + definition: + options: + 0: "OFF" + 45: "45 °C" + 46: "46 °C" + 47: "47 °C" + 48: "48 °C" + 49: "49 °C" + 50: "50 °C" + 51: "51 °C" + 52: "52 °C" + 53: "53 °C" + 54: "54 °C" + 55: "55 °C" + default: 45 + + # - title: "Min Measured Temperature" + # name: "minMeasuredTempp" + # description: "Lowest temperature recorded (°C) - Read Only" + # required: false + # preferenceType: number + # definition: + # minimum: -50 + # maximum: 100 + # default: 0 + + # - title: "Max Measured Temperature" + # name: "maxMeasuredTempp" + # description: "Highest temperature recorded (°C) - Read Only" + # required: false + # preferenceType: number + # definition: + # minimum: -50 + # maximum: 100 + # default: 0 + + + + # Tank Size not used yet + # - name: "tankSize" + # title: "Tank Size" + # description: "Select the size of the hot water tank" + # required: false + # preferenceType: enumeration + # definition: + # options: + # 40: "40 Gal" + # 50: "50 Gal" + # 60: "60 Gal" + # 80: "80 Gal" + # default: 40 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/sinope-waterheater/init.lua b/drivers/SmartThings/zigbee-switch/src/sinope-waterheater/init.lua index aa50dff907..bb7676a050 100644 --- a/drivers/SmartThings/zigbee-switch/src/sinope-waterheater/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/sinope-waterheater/init.lua @@ -1,40 +1,46 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local cluster_base = require "st.zigbee.cluster_base" -local data_types = require "st.zigbee.data_types" +-- local cluster_base = require "st.zigbee.cluster_base" +-- local data_types = require "st.zigbee.data_types" local capabilities = require "st.capabilities" -local ZigbeeDriver = require "st.zigbee" -local defaults = require "st.zigbee.defaults" +-- local ZigbeeDriver = require "st.zigbee" +-- local defaults = require "st.zigbee.defaults" local clusters = require "st.zigbee.zcl.clusters" -local log = require "log" +-- local log = require "log" +local zb_utils = require "st.zigbee.utils" -- Sinopé manufacturer cluster and attributes -local SINOPE_SWITCH_CLUSTER = 0xFF01 -local SINOPE_MAX_INTENSITY_ON_ATTRIBUTE = 0x0052 -local SINOPE_MAX_INTENSITY_OFF_ATTRIBUTE = 0x0053 -local SINOPE_CONNECTED_LOAD_ATTR = 0x0060 -- ConnectedLoad (W)[web:45] -local SINOPE_CURRENT_LOAD_ATTR = 0x0070 -- CurrentLoad (W-ish bitmap)[web:45] -local SINOPE_DR_MIN_WATER_TEMP_ATTR = 0x0076 -- drConfigWaterTempMin (°C)[web:45] -local SINOPE_DR_MIN_WATER_TEMP_TIME_ATTR = 0x0077 -- drConfigWaterTempTime[web:45] -local SINOPE_TIMER_ATTR = 0x00A0 -- Timer seconds[web:45] -local SINOPE_TIMER_COUNTDOWN_ATTR = 0x00A1 -- Timer_countDown[web:45] -local SINOPE_MIN_MEASURED_TEMP_ATTR = 0x007C -- min_measured_temp (°C×100)[web:45] -local SINOPE_MAX_MEASURED_TEMP_ATTR = 0x007D -- max_measured_temp (°C×100)[web:45] -local SINOPE_ENERGY_INTERNAL_ATTR = 0x0090 -- currentSummationDelivered (internal)[web:45] +-- local SINOPE_SWITCH_CLUSTER = 0xFF01 +-- local SINOPE_MAX_INTENSITY_ON_ATTRIBUTE = 0x0052 +-- local SINOPE_MAX_INTENSITY_OFF_ATTRIBUTE = 0x0053 +-- local SINOPE_CONNECTED_LOAD_ATTR = 0x0060 -- ConnectedLoad (W) +-- local SINOPE_CURRENT_LOAD_ATTR = 0x0070 -- CurrentLoad (W-ish bitmap) +-- local SINOPE_DR_MIN_WATER_TEMP_ATTR = 0x0076 -- drConfigWaterTempMin (°C) +-- local SINOPE_DR_MIN_WATER_TEMP_TIME_ATTR = 0x0077 -- drConfigWaterTempTime +-- local SINOPE_TIMER_ATTR = 0x00A0 -- Timer seconds +-- local SINOPE_TIMER_COUNTDOWN_ATTR = 0x00A1 -- Timer_countDown +-- local SINOPE_MIN_MEASURED_TEMP_ATTR = 0x007C -- min_measured_temp (°C×100) +-- local SINOPE_MAX_MEASURED_TEMP_ATTR = 0x007D -- max_measured_temp (°C×100) +-- local SINOPE_ENERGY_INTERNAL_ATTR = 0x0090 -- currentSummationDelivered (internal) -- Standard Zigbee clusters -local SimpleMetering = clusters.SimpleMetering -- 0x0702 -local OnOff = clusters.OnOff -- 0x0006 -local TemperatureMeasurement = clusters.TemperatureMeasurement -- 0x0402 -local IASZone = clusters.IASZone -- 0x0500 -local ElectricalMeasurement = clusters.ElectricalMeasurement -- 0x0B04 -local Thermostat = clusters.Thermostat - --- Leak state from IAS Zone 0x0500/0x0002[web:45] -local function leak_attr_handler(driver, device, value, zb_rx) - log.info("========================================") - log.info("RM3500ZB LEAK HANDLER CALLED") +local SimpleMetering = clusters.SimpleMetering -- 0x0702 +local OnOff = clusters.OnOff -- 0x0006 +local TemperatureMeasurement = clusters.TemperatureMeasurement -- 0x0402 +local IASZone = clusters.IASZone -- 0x0500 +local ElectricalMeasurement = clusters.ElectricalMeasurement -- 0x0B04 +-- local Thermostat = clusters.Thermostat + + +-- ============================================================================ +-- STANDARD ATTRIBUTE HANDLERS +-- ============================================================================ + +-- Leak state from IAS Zone 0x0500/0x0002 +local function water_sensor_handler(driver, device, value, zb_rx) + -- log.info("========================================") + -- log.info("RM3500ZB LEAK HANDLER CALLED") local zone_status = value.value local wet = (bit32.band(zone_status, 0x0001) ~= 0) -- example mask if wet then @@ -44,53 +50,49 @@ local function leak_attr_handler(driver, device, value, zb_rx) end end --- -- Active power from 0x0B04/0x050B[web:45] - local function active_power_meter_handler(driver, device, value, zb_rx) - log.info("========================================") - log.info("RM3500ZB POWERMETER HANDLER CALLED") - local raw_value = value.value - local divisor = device:get_field(constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY) or 10 - - raw_value = raw_value / divisor +-- Active power from 0x0B04/0x050B +local function active_power_meter_handler(driver, device, value, zb_rx) + -- log.info("========================================") + -- log.info("RM3500ZB POWERMETER HANDLER CALLED") - device:emit_event(capabilities.powerMeter.power({value = raw_value, unit = "W"})) - end + device:emit_event(capabilities.powerMeter.power({value = value.value, unit = "W"})) +end local function instantaneous_demand_handler(driver, device, value, zb_rx) - log.info("========================================") - log.info("RM3500ZB INSTANTMETER HANDLER CALLED") - local raw_value = value.value - local divisor = device:get_field(constants.SIMPLE_METERING_DIVISOR_KEY) or 10 - - raw_value = raw_value / divisor + -- log.info("========================================") + -- log.info("RM3500ZB INSTANTMETER HANDLER CALLED") - device:emit_event(capabilities.powerMeter.power({value = raw_value, unit = "W"})) + device:emit_event(capabilities.powerMeter.power({value = value.value, unit = "W"})) end + local function rms_voltage_handler(driver, device, value, zb_rx) - log.info("========================================") - log.info("RM3500ZB RMSVOLTS HANDLER CALLED") + -- log.info("========================================") + -- log.info("RM3500ZB RMSVOLTS HANDLER CALLED") device:emit_event(capabilities.voltageMeasurement.voltage({value = value.value, unit = "V"})) end --- local function rms_current_handler(driver, device, value, zb_rx) --- log.info("========================================") --- log.info("RM3500ZB RMS CURRENT HANDLER CALLED") --- device:emit_event(capabilities.currentMeter.current({value = value.value, unit = "A"})) --- end +local function rms_current_handler(driver, device, value, zb_rx) + -- log.info("========================================") + -- log.info("RM3500ZB RMS CURRENT HANDLER CALLED") + local raw_value = value.value + raw_value = raw_value / 1000 + + device:emit_event(capabilities.currentMeasurement.current({value = raw_value, unit = "A"})) +end local function metering_handler(driver, device, value, zb_rx) - log.info("========================================") - log.info("RM3500ZB ENERGYMETER HANDLER CALLED") + -- log.info("========================================") + -- log.info("RM3500ZB ENERGYMETER HANDLER CALLED") local raw_value = value.value / 1000 -- Convert Wh to kWh device:emit_event(capabilities.energyMeter.energy({value = raw_value, unit = "kWh"})) end local function temperature_handler(driver, device, value, zb_rx) - log.info("========================================") - log.info("RM3500ZB TEMPERATURE HANDLER CALLED") - log.info("========================================") + -- log.info("========================================") + -- log.info("RM3500ZB TEMPERATURE HANDLER CALLED") + -- log.info("========================================") local temp_c = value.value /100 - log.warn(string.format("RM3500ZB: Temperature below 0°C (%.2f°C) - ignoring", temp_c)) + -- the RM3500ZB may report 2 types of values with one in the -300 range. This only sends the value if it is over -100 deg C if temp_c > -100 then device:emit_event(capabilities.temperatureMeasurement.temperature({value = value.value / 100, unit = "C"})) return @@ -98,14 +100,136 @@ local function temperature_handler(driver, device, value, zb_rx) end -- ============================================================================ --- CONFIGURE HANDLER - Set reporting intervals +-- SINOPE MANUFACTURER ATTRIBUTE HANDLERS +-- ============================================================================ + +-- Handler for Low Temperature Protection (0xFF01/0x0076) +local function sinope_low_temp_protection_handler(driver, device, value, zb_rx) + -- log.info("========================================") + -- log.info("RM3500ZB LOW TEMP PROTECTION HANDLER CALLED") + local temp_value = value.value + -- log.info(string.format("Low temp protection value from device: %d°C", temp_value)) + + -- Store in field + device:set_field("lowTempProtection", temp_value, {persist = true}) +end + +-- -- Handler for Tank Size (0xFF01/0x0060) +-- local function sinope_tank_size_handler(driver, device, value, zb_rx) +-- log.info("========================================") +-- log.info("RM3500ZB TANK SIZE HANDLER CALLED") +-- local tank_size = value.value +-- log.info(string.format("Tank size from device: %d Gallons", tank_size)) + +-- -- Store in field +-- device:set_field("tankSize", tank_size, {persist = true}) +-- end + +-- -- Handler for Min Measured Temp (0xFF01/0x007C) - READ ONLY +-- local function sinope_min_measured_temp_handler(driver, device, value, zb_rx) +-- log.info("========================================") +-- log.info("RM3500ZB MIN MEASURED TEMP HANDLER CALLED") +-- local temp_value = value.value / 100 -- Convert from °C×100 +-- log.info(string.format("Min measured temp from device: %.2f°C", temp_value)) + +-- -- Store in field +-- device:set_field("minMeasuredTemp", temp_value, {persist = true}) + +-- -- -- Update the preference value (this makes it show in settings) +-- -- device:try_update_metadata({ +-- -- preferences = { +-- -- minMeasuredTemp = temp_value +-- -- } +-- -- }) +-- end + +-- -- Handler for Max Measured Temp (0xFF01/0x007D) - READ ONLY +-- local function sinope_max_measured_temp_handler(driver, device, value, zb_rx) +-- log.info("========================================") +-- log.info("RM3500ZB MAX MEASURED TEMP HANDLER CALLED") +-- local temp_value = value.value / 100 -- Convert from °C×100 +-- log.info(string.format("Max measured temp from device updated: %.2f°C", temp_value)) + +-- -- Store in field +-- device:set_field("maxMeasuredTemp", temp_value, {persist = true}) +-- local templog = device:get_field("maxMeasuredTemp") +-- log.info(string.format("Max measured temp from device read: %.2f°C", templog))end + +-- ============================================================================ +-- REFRESH SETTINGS FUNCTION +-- ============================================================================ + +local function refresh_settings(device) + -- log.info("========================================") + -- log.info("RM3500ZB REFRESHING SETTINGS") + -- log.info("========================================") + + -- Read Low Temperature Protection setting + device:send(cluster_base.read_manufacturer_specific_attribute( + device, + SINOPE_SWITCH_CLUSTER, + SINOPE_DR_MIN_WATER_TEMP_ATTR, + 0x119C + )) + + -- -- Read Tank Size setting (Connected Load) + -- device:send(cluster_base.read_manufacturer_specific_attribute( + -- device, + -- SINOPE_SWITCH_CLUSTER, + -- SINOPE_CONNECTED_LOAD_ATTR, + -- 0x119C + -- )) + + -- -- Read Min Measured Temp + -- device:send(cluster_base.read_manufacturer_specific_attribute( + -- device, + -- SINOPE_SWITCH_CLUSTER, + -- SINOPE_MIN_MEASURED_TEMP_ATTR, + -- 0x119C + -- )) + + -- -- Read Max Measured Temp + -- device:send(cluster_base.read_manufacturer_specific_attribute( + -- device, + -- SINOPE_SWITCH_CLUSTER, + -- SINOPE_MAX_MEASURED_TEMP_ATTR, + -- 0x119C + -- )) +end + +-- ============================================================================ +-- REFRESH ALL VALUES + SETTINGS -- ============================================================================ -local function do_configure(driver, device) - log.info("========================================") - log.info("RM3500ZB DOCONFIG HANDLER CALLED") - log.info("========================================") + +local function do_refresh(driver, device) + -- log.info("========================================") + -- log.info("RM3500ZB DOREFRESH HANDLER CALLED") + -- log.info("========================================") + -- Refresh all values + device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device)) + device:send(SimpleMetering.attributes.InstantaneousDemand:read(device)) + device:send(SimpleMetering.attributes.CurrentSummationDelivered:read(device)) + device:send(ElectricalMeasurement.attributes.ActivePower:read(device)) + device:send(ElectricalMeasurement.attributes.RMSVoltage:read(device)) + device:send(ElectricalMeasurement.attributes.RMSCurrent:read(device)) + device:send(IASZone.attributes.ZoneStatus:read(device)) + device:send(OnOff.attributes.OnOff:read(device)) + + -- Refresh settings values + refresh_settings(device) +end + +-- ============================================================================ +-- CONFIGURE HANDLER - Set reporting intervals +-- ============================================================================ + +local function configure_device(driver, device) + -- log.info("========================================") + -- log.info("RM3500ZB CONFIGURE DEVICE HANDLER CALLED") + -- log.info("========================================") + -- Configure Temperature reporting (every 30s to 5min, on 0.5°C change) device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( device, @@ -162,29 +286,82 @@ local function do_configure(driver, device) 1 -- reportable change: any change )) - -- Refresh all values after configuration - device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device)) - device:send(SimpleMetering.attributes.InstantaneousDemand:read(device)) - device:send(SimpleMetering.attributes.CurrentSummationDelivered:read(device)) - device:send(ElectricalMeasurement.attributes.RMSVoltage:read(device)) - device:send(ElectricalMeasurement.attributes.RMSCurrent:read(device)) - device:send(IASZone.attributes.ZoneStatus:read(device)) - device:send(OnOff.attributes.OnOff:read(device)) + + -- Refresh all values + settings + do_refresh(driver, device) + + end -local function do_refresh(driver, device) - log.info("========================================") - log.info("RM3500ZB DOREFRESH HANDLER CALLED") - log.info("========================================") +-- ============================================================================ +-- PREFERENCES CHANGE HANDLER +-- ============================================================================ + +local function info_changed_handler(driver, device, event, args) + -- log.info("========================================") + -- log.info("RM3500ZB INFO_CHANGED HANDLER CALLED") + -- log.info("========================================") - -- Refresh all values - device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device)) - device:send(SimpleMetering.attributes.InstantaneousDemand:read(device)) - device:send(SimpleMetering.attributes.CurrentSummationDelivered:read(device)) - device:send(ElectricalMeasurement.attributes.RMSVoltage:read(device)) - device:send(ElectricalMeasurement.attributes.RMSCurrent:read(device)) - device:send(IASZone.attributes.ZoneStatus:read(device)) - device:send(OnOff.attributes.OnOff:read(device)) + -- -- Check if this is a settings page open (no old preferences) + if not args.old_st_store.preferences then + -- log.info("Settings page opened - refreshing values") + refresh_settings(device) + return + end + + -- Handle Low Temperature Protection change + if args.old_st_store.preferences.lowTempProtection ~= device.preferences.lowTempProtection then + local new_value = tonumber(device.preferences.lowTempProtection) + -- log.info(string.format("Low temp protection changed to: %d°C", new_value)) + + -- Write to device + device:send(cluster_base.write_manufacturer_specific_attribute( + device, + SINOPE_SWITCH_CLUSTER, + SINOPE_DR_MIN_WATER_TEMP_ATTR, + 0x119C, + data_types.Uint8, + new_value + )) + + -- Read back after 2 seconds to verify + device.thread:call_with_delay(2, function() + log.info("Reading back lowTempProtection after write...") + device:send(cluster_base.read_manufacturer_specific_attribute( + device, + SINOPE_SWITCH_CLUSTER, + SINOPE_DR_MIN_WATER_TEMP_ATTR, + 0x119C + )) + end) + end + + -- -- Handle Tank Size change + -- if args.old_st_store.preferences.tankSize ~= device.preferences.tankSize then + -- local new_value = device.preferences.tankSize + -- log.info(string.format("Tank size changed to: %d Gallons", new_value)) + + -- -- Write to device + -- device:send(cluster_base.write_manufacturer_specific_attribute( + -- device, + -- SINOPE_SWITCH_CLUSTER, + -- SINOPE_CONNECTED_LOAD_ATTR, + -- 0x119C, + -- data_types.Uint16, + -- new_value + -- )) + + -- -- Read back after 2 seconds to verify + -- device.thread:call_with_delay(2, function() + -- log.info("Reading back tankSize after write...") + -- device:send(cluster_base.read_manufacturer_specific_attribute( + -- device, + -- SINOPE_SWITCH_CLUSTER, + -- SINOPE_CONNECTED_LOAD_ATTR, + -- 0x119C + -- )) + -- end) + -- end end -- ============================================================================ @@ -199,9 +376,11 @@ local sinope_rm3500zb = { } }, lifecycle_handlers = { - doConfigure = do_configure, -- Called when device is added/configured + init = configure_device, -- Called when device is added/configured + doConfigure = configure_device, + infoChanged = info_changed_handler, }, - zigbee_handlers = { + zigbee_handlers = { attr = { [ElectricalMeasurement.ID] = { [ElectricalMeasurement.attributes.ActivePower.ID] = active_power_meter_handler, @@ -215,6 +394,14 @@ local sinope_rm3500zb = { [TemperatureMeasurement.ID] = { [TemperatureMeasurement.attributes.MeasuredValue.ID] = temperature_handler }, + [IASZone.ID] = { + [IASZone.attributes.ZoneStatus.ID] = water_sensor_handler + }, + [SINOPE_SWITCH_CLUSTER] = { + [SINOPE_DR_MIN_WATER_TEMP_ATTR] = sinope_low_temp_protection_handler, + -- [SINOPE_MIN_MEASURED_TEMP_ATTR] = sinope_min_measured_temp_handler, + -- [SINOPE_MAX_MEASURED_TEMP_ATTR] = sinope_max_measured_temp_handler, + }, }, }, can_handle = require("sinope-waterheater.can_handle") @@ -222,3 +409,6 @@ local sinope_rm3500zb = { return sinope_rm3500zb + + +