diff --git a/drivers/SmartThings/zigbee-power-meter/fingerprints.yml b/drivers/SmartThings/zigbee-power-meter/fingerprints.yml index ec58259dd5..b6fe6d2b9d 100644 --- a/drivers/SmartThings/zigbee-power-meter/fingerprints.yml +++ b/drivers/SmartThings/zigbee-power-meter/fingerprints.yml @@ -7,12 +7,22 @@ zigbeeManufacturer: deviceLabel: frient Energy Monitor manufacturer: Develco model: "ZHEMI101" - deviceProfileName: power-meter - - id: "Develco/EMIZB-132" + deviceProfileName: power-energy-consumption-report + - id: "frient A/S/EMIZB-132" deviceLabel: frient Energy Monitor - manufacturer: Develco Products A/S + manufacturer: frient A/S model: "EMIZB-132" - deviceProfileName: power-meter + deviceProfileName: power-meter-consumption-report + - id: "frient A/S/EMIZB-141" + deviceLabel: "frient EMI 2 LED" + manufacturer: frient A/S + model: "EMIZB-141" + deviceProfileName: power-energy-battery-consumption-report + - id: "frient A/S/EMIZB-151" + deviceLabel: "frient EMI 2 P1" + manufacturer: frient A/S + model: "EMIZB-151" + deviceProfileName: power-energy-current-voltage - id: "ShinaSystem/PMM-300Z1" deviceLabel: SiHAS Energy Monitor manufacturer: ShinaSystem diff --git a/drivers/SmartThings/zigbee-power-meter/profiles/power-energy-battery-consumption-report.yml b/drivers/SmartThings/zigbee-power-meter/profiles/power-energy-battery-consumption-report.yml new file mode 100644 index 0000000000..5857c34557 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/profiles/power-energy-battery-consumption-report.yml @@ -0,0 +1,37 @@ +name: power-energy-battery-consumption-report +components: + - id: main + capabilities: + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter +preferences: + - title: "Pulse Configuration" + name: pulseConfiguration + description: "Number of pulses the meter outputs per unit" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 10000 + default: 1000 + - title: "Initial Energy Consumption" + name: currentSummation + description: "Offset (scaled value) for current summation delivered" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 268435455 + default: 0 diff --git a/drivers/SmartThings/zigbee-power-meter/profiles/power-energy-consumption-report.yml b/drivers/SmartThings/zigbee-power-meter/profiles/power-energy-consumption-report.yml new file mode 100644 index 0000000000..82e7e4bf34 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/profiles/power-energy-consumption-report.yml @@ -0,0 +1,35 @@ +name: power-energy-consumption-report +components: + - id: main + capabilities: + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter +preferences: + - title: "Pulse Configuration" + name: pulseConfiguration + description: "Number of pulses the meter outputs per unit" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 10000 + default: 1000 + - title: "Initial Energy Consumption" + name: currentSummation + description: "Offset (scaled value) for current summation delivered" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 268435455 + default: 0 diff --git a/drivers/SmartThings/zigbee-power-meter/profiles/power-energy-current-voltage.yml b/drivers/SmartThings/zigbee-power-meter/profiles/power-energy-current-voltage.yml new file mode 100644 index 0000000000..da752f6011 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/profiles/power-energy-current-voltage.yml @@ -0,0 +1,48 @@ +name: power-energy-current-voltage +components: + - id: main + capabilities: + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter + - id: production + label: Production + capabilities: + - id: energyMeter + version: 1 + - id: phaseA + label: "Phase A" + capabilities: + - id: powerMeter + version: 1 + - id: voltageMeasurement + version: 1 + - id: currentMeasurement + version: 1 + - id: phaseB + label: "Phase B" + capabilities: + - id: powerMeter + version: 1 + - id: voltageMeasurement + version: 1 + - id: currentMeasurement + version: 1 + - id: phaseC + label: "Phase C" + capabilities: + - id: powerMeter + version: 1 + - id: voltageMeasurement + version: 1 + - id: currentMeasurement + version: 1 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/can_handle.lua b/drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/can_handle.lua new file mode 100644 index 0000000000..3fd0371aca --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_frient_power_meter = function(opts, driver, device) + local FINGERPRINTS = require("frient/EMIZB-151.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return true, require("frient/EMIZB-151") + end + end + + return false +end + +return is_frient_power_meter diff --git a/drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/fingerprints.lua b/drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/fingerprints.lua new file mode 100644 index 0000000000..6530d4e7fd --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_POWER_METER_FINGERPRINTS = { + { model = "EMIZB-151"} +} + +return ZIGBEE_POWER_METER_FINGERPRINTS \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/init.lua b/drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/init.lua new file mode 100644 index 0000000000..5c348ed744 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/init.lua @@ -0,0 +1,362 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local zigbee_constants = require "st.zigbee.constants" +local capabilities = require "st.capabilities" + +local clusters = require "st.zigbee.zcl.clusters" +local SimpleMetering = clusters.SimpleMetering +local ElectricalMeasurement = clusters.ElectricalMeasurement +local utils = require "frient.utils" + +local data_types = require "st.zigbee.data_types" +local LAST_REPORT_TIME = "LAST_REPORT_TIME" +local SIMPLE_METERING_DEFAULT_DIVISOR = 1000 + +zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_MULTIPLIER_KEY = "_electrical_measurement_ac_voltage_multiplier" +zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_MULTIPLIER_KEY = "_electrical_measurement_ac_current_multiplier" +zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_DIVISOR_KEY = "_electrical_measurement_ac_voltage_divisor" +zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_DIVISOR_KEY = "_electrical_measurement_ac_current_divisor" + +local CurrentSummationReceived = 0x0001 + +local ATTRIBUTES = { + { + cluster = SimpleMetering.ID, + attribute = CurrentSummationReceived, + minimum_interval = 5, + maximum_interval = 3600, + data_type = data_types.Uint48, + reportable_change = 1 + }, + { + cluster = SimpleMetering.ID, + attribute = SimpleMetering.attributes.CurrentSummationDelivered.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = data_types.Uint48, + reportable_change = 1 + }, + { + cluster = SimpleMetering.ID, + attribute = SimpleMetering.attributes.InstantaneousDemand.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = data_types.Int24, + reportable_change = 1 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.ActivePower.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = data_types.Int16, + reportable_change = 5 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.ActivePowerPhB.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = data_types.Int16, + reportable_change = 5 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.ActivePowerPhC.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = data_types.Int16, + reportable_change = 5 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSVoltage.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = data_types.Uint16, + reportable_change = 5 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSVoltagePhB.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = data_types.Uint16, + reportable_change = 5 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSVoltagePhC.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = data_types.Uint16, + reportable_change = 5 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSCurrent.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = data_types.Uint16, + reportable_change = 5 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSCurrentPhB.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = data_types.Uint16, + reportable_change = 5 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSCurrentPhC.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = data_types.Uint16, + reportable_change = 5 + } +} + +local device_init = function(self, device) + for _, attribute in ipairs(ATTRIBUTES) do + device:add_configured_attribute(attribute) + end +end + +local do_configure = function(self, device) + device:refresh() + device:configure() + + -- Divisor and multipler for PowerMeter + device:send(SimpleMetering.attributes.Divisor:read(device)) + device:send(SimpleMetering.attributes.Multiplier:read(device)) + + -- Divisor and multipler for EnergyMeter + device:send(ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) + device:send(ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) + device:send(ElectricalMeasurement.attributes.ACVoltageMultiplier:read(device)) + device:send(ElectricalMeasurement.attributes.ACVoltageDivisor:read(device)) + device:send(ElectricalMeasurement.attributes.ACCurrentMultiplier:read(device)) + device:send(ElectricalMeasurement.attributes.ACCurrentDivisor:read(device)) +end + +local instantaneous_demand_handler = function(driver, device, value, zb_rx) + local raw_value = value.value + local multiplier = device:get_field(zigbee_constants.SIMPLE_METERING_MULTIPLIER_KEY) or 1 + local divisor = device:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) or SIMPLE_METERING_DEFAULT_DIVISOR + + if divisor == 0 then + divisor = 1 + end + + raw_value = raw_value * multiplier / divisor * 1000 + + -- The result is already in watts, no need to multiply by 1000 + device:emit_component_event(device.profile.components['main'], capabilities.powerMeter.power({ value = raw_value, unit = "W" })) +end + +local current_summation_delivered_handler = function(driver, device, value, zb_rx) + local raw_value = value.value + + -- Handle potential overflow values + if raw_value < 0 or raw_value >= 0xFFFFFFFFFFFF then + return + end + + local multiplier = device:get_field(zigbee_constants.SIMPLE_METERING_MULTIPLIER_KEY) or 1 + local divisor = device:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) or SIMPLE_METERING_DEFAULT_DIVISOR + + if divisor == 0 then + divisor = 1 + end + + raw_value = raw_value * multiplier / divisor * 1000 + device:emit_component_event(device.profile.components['main'], capabilities.energyMeter.energy({ value = raw_value, unit = "Wh" })) + + local delta_energy = 0.0 + local current_power_consumption = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, capabilities.powerConsumptionReport.powerConsumption.NAME) + if current_power_consumption ~= nil then + delta_energy = math.max(raw_value - current_power_consumption.energy, 0.0) + end + + local current_time = os.time() + local last_report_time = device:get_field(LAST_REPORT_TIME) or 0 + local next_report_time = last_report_time + 60 * 15 -- 15 mins, the minimum interval allowed between reports + if current_time < next_report_time then + return + end + + device:emit_event_for_endpoint( + zb_rx.address_header.src_endpoint.value, + capabilities.powerConsumptionReport.powerConsumption({ + start = utils.epoch_to_iso8601(last_report_time), + ["end"] = utils.epoch_to_iso8601(current_time - 1), + deltaEnergy = delta_energy, + energy = raw_value + }) + ) + device:set_field(LAST_REPORT_TIME, current_time, { persist = true }) +end + +local current_summation_received_handler = function(driver, device, value, zb_rx) + local raw_value = value.value + + -- Handle potential overflow values + if raw_value < 0 or raw_value >= 0xFFFFFFFFFFFF then + return + end + + local multiplier = device:get_field(zigbee_constants.SIMPLE_METERING_MULTIPLIER_KEY) or 1 + local divisor = device:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) or 1000 + + if divisor == 0 then + divisor = 1 + end + + raw_value = raw_value * multiplier / divisor * 1000 + device:emit_component_event(device.profile.components['production'], capabilities.energyMeter.energy({ value = raw_value, unit = "Wh" })) +end + +local electrical_measurement_ac_voltage_multiplier_handler = function(driver, device, multiplier, zb_rx) + local raw_value = multiplier.value + device:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_MULTIPLIER_KEY, raw_value, { persist = true }) +end + +local electrical_measurement_ac_voltage_divisor_handler = function(driver, device, divisor, zb_rx) + local raw_value = divisor.value + device:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_DIVISOR_KEY, raw_value, { persist = true }) + device:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_DIVISOR_KEY, raw_value, { persist = true }) +end + +local electrical_measurement_ac_current_multiplier_handler = function(driver, device, multiplier, zb_rx) + local raw_value = multiplier.value + device:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_MULTIPLIER_KEY, raw_value, { persist = true }) +end + +local electrical_measurement_ac_current_divisor_handler = function(driver, device, divisor, zb_rx) + local raw_value = divisor.value + device:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_DIVISOR_KEY, raw_value, { persist = true }) +end + +local active_power_handler = function(component) + local handler = function(driver, device, value, zb_rx) + local raw_value = value.value + -- By default emit raw value + local multiplier = device:get_field(zigbee_constants.ELECTRICAL_MEASUREMENT_MULTIPLIER_KEY) or 1 + local divisor = device:get_field(zigbee_constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY) or 1 + + if divisor == 0 then + divisor = 1 + end + + raw_value = raw_value * multiplier / divisor + + device:emit_component_event(device.profile.components[component], capabilities.powerMeter.power({ value = raw_value, unit = "W" })) + end + + return handler +end + +local rms_voltage_handler = function(component) + local handler = function(driver, device, value, zb_rx) + local raw_value = value.value + -- By default emit raw value + local multiplier = device:get_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_MULTIPLIER_KEY) or 1 + local divisor = device:get_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_DIVISOR_KEY) or 1 + + if divisor == 0 then + divisor = 1 + end + + raw_value = raw_value * multiplier / divisor + + device:emit_component_event(device.profile.components[component], capabilities.voltageMeasurement.voltage({ value = raw_value, unit = "V" })) + end + + return handler +end + +local rms_current_handler = function(component) + local handler = function(driver, device, value, zb_rx) + local raw_value = value.value + -- By default emit raw value + local multiplier = device:get_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_MULTIPLIER_KEY) or 1 + local divisor = device:get_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_DIVISOR_KEY) or 1 + + if divisor == 0 then + divisor = 1 + end + + raw_value = raw_value * multiplier / divisor + + device:emit_component_event(device.profile.components[component], capabilities.currentMeasurement.current({ value = raw_value, unit = "A" })) + end + + return handler +end + +local function simple_metering_divisor_handler(driver, device, divisor, zb_rx) + local header = zb_rx.body and zb_rx.body.zcl_header + local is_mfg_specific = header and header.frame_ctrl:is_mfg_specific_set() + local has_expected_type = divisor ~= nil and divisor.ID == data_types.Uint24.ID + + if is_mfg_specific or not has_expected_type then + return + end + + local raw_value = divisor.value + + if raw_value == 0 then + raw_value = 1 + end + + device:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, raw_value, { persist = true }) +end + +local function simple_metering_multiplier_handler(driver, device, multiplier, zb_rx) + if not zb_rx.body.zcl_header.frame_ctrl:is_mfg_specific_set() then + local raw_value = multiplier.value + device:set_field(zigbee_constants.SIMPLE_METERING_MULTIPLIER_KEY, raw_value, { persist = true }) + end +end + +local frient_emi = { + NAME = "EMIZB-151", + lifecycle_handlers = { + init = device_init, + doConfigure = do_configure + }, + zigbee_handlers = { + cluster = { + }, + attr = { + [SimpleMetering.ID] = { + [CurrentSummationReceived] = current_summation_received_handler, + [SimpleMetering.attributes.CurrentSummationDelivered.ID] = current_summation_delivered_handler, + [SimpleMetering.attributes.InstantaneousDemand.ID] = instantaneous_demand_handler, + [SimpleMetering.attributes.Multiplier.ID] = simple_metering_multiplier_handler, + [SimpleMetering.attributes.Divisor.ID] = simple_metering_divisor_handler + }, + [ElectricalMeasurement.ID] = { + [ElectricalMeasurement.attributes.ACVoltageDivisor.ID] = electrical_measurement_ac_voltage_divisor_handler, + [ElectricalMeasurement.attributes.ACVoltageMultiplier.ID] = electrical_measurement_ac_voltage_multiplier_handler, + [ElectricalMeasurement.attributes.ACCurrentDivisor.ID] = electrical_measurement_ac_current_divisor_handler, + [ElectricalMeasurement.attributes.ACCurrentMultiplier.ID] = electrical_measurement_ac_current_multiplier_handler, + [ElectricalMeasurement.attributes.ActivePower.ID] = active_power_handler("phaseA"), + [ElectricalMeasurement.attributes.RMSVoltage.ID] = rms_voltage_handler("phaseA"), + [ElectricalMeasurement.attributes.RMSCurrent.ID] = rms_current_handler("phaseA"), + [ElectricalMeasurement.attributes.ActivePowerPhB.ID] = active_power_handler("phaseB"), + [ElectricalMeasurement.attributes.RMSVoltagePhB.ID] = rms_voltage_handler("phaseB"), + [ElectricalMeasurement.attributes.RMSCurrentPhB.ID] = rms_current_handler("phaseB"), + [ElectricalMeasurement.attributes.ActivePowerPhC.ID] = active_power_handler("phaseC"), + [ElectricalMeasurement.attributes.RMSVoltagePhC.ID] = rms_voltage_handler("phaseC"), + [ElectricalMeasurement.attributes.RMSCurrentPhC.ID] = rms_current_handler("phaseC") + } + } + }, + can_handle = require("frient/EMIZB-151.can_handle") +} + +return frient_emi \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/src/frient/fingerprints.lua b/drivers/SmartThings/zigbee-power-meter/src/frient/fingerprints.lua index 5bc09f600d..c9e700c576 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/frient/fingerprints.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/fingerprints.lua @@ -2,8 +2,9 @@ -- Licensed under the Apache License, Version 2.0 local ZIGBEE_POWER_METER_FINGERPRINTS = { - { model = "ZHEMI101" }, - { model = "EMIZB-132" }, + { model = "ZHEMI101", preferences = true, battery = false }, + { model = "EMIZB-132", preferences = false, battery = false }, + { model = "EMIZB-141", preferences = true, battery = true, MIN_BAT = 2.3 , MAX_BAT = 3.0 } } return ZIGBEE_POWER_METER_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua b/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua index 5933faf5cb..f47b156e87 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua @@ -1,27 +1,207 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local zigbee_constants = require "st.zigbee.constants" +local capabilities = require "st.capabilities" +local cluster_base = require "st.zigbee.cluster_base" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" -local constants = require "st.zigbee.constants" -local configurations = require "configurations" +local clusters = require "st.zigbee.zcl.clusters" +local SimpleMetering = clusters.SimpleMetering +local PowerConfiguration = clusters.PowerConfiguration +local utils = require "frient.utils" +local LAST_REPORT_TIME = "LAST_REPORT_TIME" +local data_types = require "st.zigbee.data_types" + +local log = require "log" +local DEVELCO_MANUFACTURER_CODE = 0x1015 +local SIMPLE_METERING_DEFAULT_DIVISOR = 1000 + +local ZIGBEE_POWER_METER_FINGERPRINTS = require("frient.fingerprints") + +local ATTRIBUTES = { + { + cluster = SimpleMetering.ID, + attribute = SimpleMetering.attributes.CurrentSummationDelivered.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = data_types.Uint48, + reportable_change = 1 + }, + { + cluster = SimpleMetering.ID, + attribute = SimpleMetering.attributes.InstantaneousDemand.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = data_types.Int24, + reportable_change = 1 + } +} + +local device_init = function(self, device) + for _, fingerprint in ipairs(ZIGBEE_POWER_METER_FINGERPRINTS) do + if device:get_model() == fingerprint.model and fingerprint.battery then + battery_defaults.build_linear_voltage_init(fingerprint.MIN_BAT, fingerprint.MAX_BAT)(self, device) + end + end + for _, attribute in ipairs(ATTRIBUTES) do + device:add_configured_attribute(attribute) + end +end + +local do_refresh = function(self, device) + device:refresh() + if device:supports_capability(capabilities.battery) then + device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) + end +end local do_configure = function(self, device) device:refresh() device:configure() + + if device:supports_capability(capabilities.battery) then + device:send(PowerConfiguration.attributes.BatteryVoltage:configure_reporting(device, 30, 21600, 1)) + end + for _, fingerprint in ipairs(ZIGBEE_POWER_METER_FINGERPRINTS) do + if device:get_model() == fingerprint.model and fingerprint.preferences then + local pulseConfiguration = tonumber(device.preferences.pulseConfiguration) or 1000 + device:send(cluster_base.write_manufacturer_specific_attribute(device, SimpleMetering.ID, 0x0300, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, pulseConfiguration):to_endpoint(0x02)) + + local currentSummation = tonumber(device.preferences.currentSummation) or 0 + device:send(cluster_base.write_manufacturer_specific_attribute(device, SimpleMetering.ID, 0x0301, DEVELCO_MANUFACTURER_CODE, data_types.Uint48, currentSummation):to_endpoint(0x02)) + end + end + + -- Divisor and multipler for PowerMeter + device:send(SimpleMetering.attributes.Divisor:read(device)) + device:send(SimpleMetering.attributes.Multiplier:read(device)) + + device.thread:call_with_delay(5, function() + do_refresh(self, device) + end) end -local device_init = function(self, device) - device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) - device:set_field(constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10000, {persist = true}) +local function info_changed(driver, device, event, args) + for name, value in pairs(device.preferences) do + if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then + if (name == "pulseConfiguration") then + local pulseConfiguration = tonumber(device.preferences.pulseConfiguration) + device:send(cluster_base.write_manufacturer_specific_attribute(device, SimpleMetering.ID, 0x0300, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, pulseConfiguration):to_endpoint(0x02)) + end + if (name == "currentSummation") then + local currentSummation = tonumber(device.preferences.currentSummation) + device:send(cluster_base.write_manufacturer_specific_attribute(device, SimpleMetering.ID, 0x0301, DEVELCO_MANUFACTURER_CODE, data_types.Uint48, currentSummation):to_endpoint(0x02)) + end + end + end + device.thread:call_with_delay(5, function() + do_refresh(driver, device) + end) +end + +local function simple_metering_divisor_handler(driver, device, divisor, zb_rx) + local new_divisor = SIMPLE_METERING_DEFAULT_DIVISOR + local header = zb_rx.body and zb_rx.body.zcl_header + if header and header.frame_ctrl:is_mfg_specific_set() then + log.debug_with({ hub_logs = true }, string.format("Ignoring manufacturer-specific divisor report: %s", tostring(divisor.value))) + elseif (divisor.value and divisor.value == 0) then + log.warn_with({ hub_logs = true }, "Simple metering divisor reported as 0; forcing divisor to 1000") + elseif (divisor.value and divisor.value > 0) then + new_divisor = divisor.value + end + device:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, new_divisor, { persist = true }) +end + +local function instantaneous_demand_handler(driver, device, value, zb_rx) + local raw_value = value.value + --- demand = demand received * Multipler/Divisor + local multiplier = device:get_field(zigbee_constants.SIMPLE_METERING_MULTIPLIER_KEY) or 1 + local divisor = device:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) or SIMPLE_METERING_DEFAULT_DIVISOR + if raw_value < -8388607 or raw_value >= 8388607 then + raw_value = 0 + end + + raw_value = raw_value * multiplier / divisor * 1000 + + local raw_value_watts = raw_value + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.powerMeter.power({ value = raw_value_watts, unit = "W" })) +end + +local function energy_meter_handler(driver, device, value, zb_rx) + local raw_value = value.value + local multiplier = device:get_field(zigbee_constants.SIMPLE_METERING_MULTIPLIER_KEY) or 1 + local divisor = device:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) or SIMPLE_METERING_DEFAULT_DIVISOR + + if raw_value < 0 or raw_value >= 0xFFFFFFFFFFFF then + return + end + + raw_value = (raw_value * multiplier) / divisor + + local offset = device:get_field(zigbee_constants.ENERGY_METER_OFFSET) or 0 + if raw_value < offset then + --- somehow our value has gone below the offset, so we'll reset the offset, since the device seems to have + offset = 0 + device:set_field(zigbee_constants.ENERGY_METER_OFFSET, offset, { persist = true }) + end + raw_value = raw_value - offset + raw_value = raw_value * 1000 -- the unit of these values should be 'Wh' + + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.energyMeter.energy({ value = raw_value, unit = "Wh" })) + + local delta_energy = 0.0 + local current_power_consumption = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, capabilities.powerConsumptionReport.powerConsumption.NAME) + if current_power_consumption ~= nil then + delta_energy = math.max(raw_value - current_power_consumption.energy, 0.0) + end + + local current_time = os.time() + local last_report_time = device:get_field(LAST_REPORT_TIME) or 0 + local next_report_time = last_report_time + 60 * 15 -- 15 mins, the minimum interval allowed between reports + if current_time < next_report_time then + return + end + + device:emit_event_for_endpoint( + zb_rx.address_header.src_endpoint.value, + capabilities.powerConsumptionReport.powerConsumption({ + start = utils.epoch_to_iso8601(last_report_time), + ["end"] = utils.epoch_to_iso8601(current_time - 1), + deltaEnergy = delta_energy, + energy = raw_value + }) + ) + device:set_field(LAST_REPORT_TIME, current_time, { persist = true }) end local frient_power_meter_handler = { NAME = "frient power meter handler", lifecycle_handlers = { - init = configurations.power_reconfig_wrapper(device_init), + init = device_init, doConfigure = do_configure, + infoChanged = info_changed + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + } + }, + zigbee_handlers = { + cluster = { + }, + attr = { + [SimpleMetering.ID] = { + [SimpleMetering.attributes.CurrentSummationDelivered.ID] = energy_meter_handler, + [SimpleMetering.attributes.InstantaneousDemand.ID] = instantaneous_demand_handler, + [SimpleMetering.attributes.Divisor.ID] = simple_metering_divisor_handler + } + } + }, + sub_drivers = { + require("frient/EMIZB-151") }, can_handle = require("frient.can_handle"), } diff --git a/drivers/SmartThings/zigbee-power-meter/src/frient/utils.lua b/drivers/SmartThings/zigbee-power-meter/src/frient/utils.lua new file mode 100644 index 0000000000..313ef19d50 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/utils.lua @@ -0,0 +1,21 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local utils = {} + +utils.epoch_to_iso8601 = function(time) + return os.date("!%Y-%m-%dT%H:%M:%SZ", time) +end + +return utils \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/src/init.lua b/drivers/SmartThings/zigbee-power-meter/src/init.lua index f15fae7905..6aa3d4b8c1 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/init.lua @@ -41,6 +41,7 @@ local zigbee_power_meter_driver_template = { capabilities.powerMeter, capabilities.energyMeter, capabilities.powerConsumptionReport, + capabilities.battery, }, zigbee_handlers = { global = { diff --git a/drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua b/drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua index 51b24aca32..3b53c5b2b5 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua @@ -7,5 +7,6 @@ local sub_drivers = { lazy_load_if_possible("frient"), lazy_load_if_possible("shinasystems"), lazy_load_if_possible("bituo"), + lazy_load_if_possible("frient/EMIZB-151") } return sub_drivers diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_battery_consumption_report.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_battery_consumption_report.lua new file mode 100644 index 0000000000..ae503638c8 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_battery_consumption_report.lua @@ -0,0 +1,340 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Mock out globals +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering +local PowerConfiguration = clusters.PowerConfiguration +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" + +local DEVELCO_MANUFACTURER_CODE = 0x1015 +local LAST_REPORT_TIME = "LAST_REPORT_TIME" + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("power-energy-battery-consumption-report.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + model = "EMIZB-141", + server_clusters = { ElectricalMeasurement.ID, PowerConfiguration.ID, SimpleMetering.ID } + } + } + } +) + + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "InstantaneousDemand Report should be handled.", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, SimpleMetering.attributes.Divisor:build_test_attr_report(mock_device, 1000) } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, SimpleMetering.attributes.Multiplier:build_test_attr_report(mock_device, 1) }, + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, SimpleMetering.attributes.InstantaneousDemand:build_test_attr_report(mock_device, 2700) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 2700.0, unit = "W" })) + } + } +) + +test.register_coroutine_test( + "lifecycle configure event should configure the device", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, SimpleMetering.ID, 0x0300, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 1000):to_endpoint(0x02) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, SimpleMetering.ID, 0x0301, DEVELCO_MANUFACTURER_CODE, data_types.Uint48, 0):to_endpoint(0x02) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.Divisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.Multiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:read(mock_device) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device, 30, 21600, 1 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + SimpleMetering.ID + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + ElectricalMeasurement.ID + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting( + mock_device, 5, 3600, 1 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting( + mock_device, 1, 43200, 1 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting( + mock_device, 1, 43200, 1 + ) + }) + + + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting( + mock_device, 5, 3600, 5 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting( + mock_device, 5, 3600, 1 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device, 30, 21600, 1 + ) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Refresh should read all necessary attributes", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} }} + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, SimpleMetering.attributes.InstantaneousDemand:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, ElectricalMeasurement.attributes.ActivePower:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) } + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_coroutine_test( + "infochanged to check for necessary preferences settings: pulseConfiguration, currentSummation", + function() + local updates = { + preferences = { + pulseConfiguration = 400, + currentSummation = 500 + } + } + + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.write_manufacturer_specific_attribute( + mock_device, + SimpleMetering.ID, + 0x0300, + DEVELCO_MANUFACTURER_CODE, + data_types.Uint16, + 400 + ):to_endpoint(0x02) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.write_manufacturer_specific_attribute( + mock_device, + SimpleMetering.ID, + 0x0301, + DEVELCO_MANUFACTURER_CODE, + data_types.Uint48, + 500 + ):to_endpoint(0x02) + }) + + test.socket.zigbee:__set_channel_ordering("relaxed") + + end +) + +test.register_coroutine_test( + "CurrentSummationDelivered Report should be handled.", + function() + local current_time = os.time() - 60 * 16 + mock_device:set_field(LAST_REPORT_TIME, current_time) + + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.Divisor:build_test_attr_report(mock_device, 1000) }) + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.Multiplier:build_test_attr_report(mock_device, 1) }) + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 2700) }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 2700.0, unit = "Wh" })) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.powerConsumptionReport.powerConsumption({ + start = "1969-12-31T23:44:00Z", + ["end"] = "1969-12-31T23:59:59Z", + deltaEnergy = 0.0, + energy = 2700.0 + }) + ) + ) + end +) + +test.register_coroutine_test( + "CurrentSummationDelivered report should be handled without powerConsumptionReport because 15 min didn't pass since last report", + function() + local current_time = os.time() - 60 * 14 + mock_device:set_field(LAST_REPORT_TIME, current_time) + + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.Divisor:build_test_attr_report(mock_device, 1000) }) + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.Multiplier:build_test_attr_report(mock_device, 1) }) + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 2700) }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 2700.0, unit = "Wh" })) + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_consumption_report.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_consumption_report.lua new file mode 100644 index 0000000000..17c0ce071d --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_consumption_report.lua @@ -0,0 +1,303 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Mock out globals +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering +local PowerConfiguration = clusters.PowerConfiguration +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" + +local DEVELCO_MANUFACTURER_CODE = 0x1015 +local LAST_REPORT_TIME = "LAST_REPORT_TIME" + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("power-energy-consumption-report.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + model = "ZHEMI101", + server_clusters = { ElectricalMeasurement.ID, PowerConfiguration.ID, SimpleMetering.ID } + } + } + } +) + + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "InstantaneousDemand Report should be handled.", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, SimpleMetering.attributes.Divisor:build_test_attr_report(mock_device, 1000) } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, SimpleMetering.attributes.Multiplier:build_test_attr_report(mock_device, 1) }, + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, SimpleMetering.attributes.InstantaneousDemand:build_test_attr_report(mock_device, 2700) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 2700.0, unit = "W" })) + } + } +) + +test.register_coroutine_test( + "lifecycle configure event should configure the device", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, SimpleMetering.ID, 0x0300, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 1000):to_endpoint(0x02) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, SimpleMetering.ID, 0x0301, DEVELCO_MANUFACTURER_CODE, data_types.Uint48, 0):to_endpoint(0x02) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.Divisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.Multiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:read(mock_device) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + SimpleMetering.ID + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + ElectricalMeasurement.ID + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting( + mock_device, 5, 3600, 1 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting( + mock_device, 1, 43200, 1 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting( + mock_device, 1, 43200, 1 + ) + }) + + + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting( + mock_device, 5, 3600, 5 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting( + mock_device, 5, 3600, 1 + ) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Refresh should read all necessary attributes", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} }} + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, SimpleMetering.attributes.InstantaneousDemand:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, ElectricalMeasurement.attributes.ActivePower:read(mock_device) } + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_coroutine_test( + "infochanged to check for necessary preferences settings: pulseConfiguration, currentSummation", + function() + local updates = { + preferences = { + pulseConfiguration = 400, + currentSummation = 500 + } + } + + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.write_manufacturer_specific_attribute( + mock_device, + SimpleMetering.ID, + 0x0300, + DEVELCO_MANUFACTURER_CODE, + data_types.Uint16, + 400 + ):to_endpoint(0x02) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.write_manufacturer_specific_attribute( + mock_device, + SimpleMetering.ID, + 0x0301, + DEVELCO_MANUFACTURER_CODE, + data_types.Uint48, + 500 + ):to_endpoint(0x02) + }) + + test.socket.zigbee:__set_channel_ordering("relaxed") + + end +) + +test.register_coroutine_test( + "CurrentSummationDelivered Report should be handled.", + function() + local current_time = os.time() - 60 * 16 + mock_device:set_field(LAST_REPORT_TIME, current_time) + + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.Divisor:build_test_attr_report(mock_device, 1000) }) + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.Multiplier:build_test_attr_report(mock_device, 1) }) + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 2700) }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 2700.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.powerConsumptionReport.powerConsumption({ + start = "1969-12-31T23:44:00Z", + ["end"] = "1969-12-31T23:59:59Z", + deltaEnergy = 0.0, + energy = 2700.0 + }) + ) + ) + end +) + +test.register_coroutine_test( + "CurrentSummationDelivered report should be handled without powerConsumptionReport because 15 min didn't pass since last report", + function() + local current_time = os.time() - 60 * 14 + mock_device:set_field(LAST_REPORT_TIME, current_time) + + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.Divisor:build_test_attr_report(mock_device, 1000) }) + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.Multiplier:build_test_attr_report(mock_device, 1) }) + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 2700) }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 2700.0, unit = "Wh" })) + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_current_voltage.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_current_voltage.lua new file mode 100644 index 0000000000..7dd7fbc336 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_current_voltage.lua @@ -0,0 +1,396 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Mock out globals +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" + +local CurrentSummationReceived = 0x0001 +local LAST_REPORT_TIME = "LAST_REPORT_TIME" + +local zigbee_constants = require "st.zigbee.constants" +zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_MULTIPLIER_KEY = "_electrical_measurement_ac_voltage_multiplier" +zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_MULTIPLIER_KEY = "_electrical_measurement_ac_current_multiplier" +zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_DIVISOR_KEY = "_electrical_measurement_ac_voltage_divisor" +zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_DIVISOR_KEY = "_electrical_measurement_ac_current_divisor" + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("power-energy-current-voltage.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + model = "EMIZB-151", + server_clusters = { ElectricalMeasurement.ID, SimpleMetering.ID } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +local function expected_refresh_commands() + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_attribute( + mock_device, + data_types.ClusterId(SimpleMetering.ID), + CurrentSummationReceived + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhC:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhC:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhC:read(mock_device) + }) +end + + + + +test.register_coroutine_test( + "Refresh should read all necessary attributes", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "refresh", command = "refresh", args = {} } }) + + expected_refresh_commands() + end +) + +test.register_coroutine_test( + "ALl reports (for all phases) should be handled properly", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.Multiplier:build_test_attr_report(mock_device, 1) }) + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.Divisor:build_test_attr_report(mock_device, 1000) }) + test.socket.zigbee:__queue_receive({ mock_device.id, ElectricalMeasurement.attributes.ACVoltageMultiplier:build_test_attr_report(mock_device, 1) }) + test.socket.zigbee:__queue_receive({ mock_device.id, ElectricalMeasurement.attributes.ACVoltageDivisor:build_test_attr_report(mock_device, 1000) }) + test.socket.zigbee:__queue_receive({ mock_device.id, ElectricalMeasurement.attributes.ACCurrentMultiplier:build_test_attr_report(mock_device, 1) }) + test.socket.zigbee:__queue_receive({ mock_device.id, ElectricalMeasurement.attributes.ACCurrentDivisor:build_test_attr_report(mock_device, 1000) }) + + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 30) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 30.0, unit = "Wh"})) + ) + + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.InstantaneousDemand:build_test_attr_report(mock_device, 40) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 40.0, unit = "W"})) + ) + + test.socket.zigbee:__queue_receive({ mock_device.id, ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_device, 50) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("phaseA", capabilities.powerMeter.power({ value = 50.0, unit = "W"})) + ) + + test.socket.zigbee:__queue_receive({ mock_device.id, ElectricalMeasurement.attributes.RMSVoltage:build_test_attr_report(mock_device, 50) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("phaseA", capabilities.voltageMeasurement.voltage({ value = 0.05, unit = "V"})) + ) + + test.socket.zigbee:__queue_receive({ mock_device.id, ElectricalMeasurement.attributes.RMSCurrent:build_test_attr_report(mock_device, 60) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("phaseA", capabilities.currentMeasurement.current({ value = 0.06, unit = "A"})) + ) + + test.socket.zigbee:__queue_receive({ mock_device.id, ElectricalMeasurement.attributes.ActivePowerPhB:build_test_attr_report(mock_device, 70) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("phaseB", capabilities.powerMeter.power({ value = 70.0, unit = "W"})) + ) + + test.socket.zigbee:__queue_receive({ mock_device.id, ElectricalMeasurement.attributes.RMSVoltagePhB:build_test_attr_report(mock_device, 80) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("phaseB", capabilities.voltageMeasurement.voltage({ value = 0.08, unit = "V"})) + ) + + test.socket.zigbee:__queue_receive({ mock_device.id, ElectricalMeasurement.attributes.RMSCurrentPhB:build_test_attr_report(mock_device, 90) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("phaseB", capabilities.currentMeasurement.current({ value = 0.09, unit = "A"})) + ) + + test.socket.zigbee:__queue_receive({ mock_device.id, ElectricalMeasurement.attributes.ActivePowerPhC:build_test_attr_report(mock_device, 100) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("phaseC", capabilities.powerMeter.power({ value = 100.0, unit = "W"})) + ) + + test.socket.zigbee:__queue_receive({ mock_device.id, ElectricalMeasurement.attributes.RMSVoltagePhC:build_test_attr_report(mock_device, 110) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("phaseC", capabilities.voltageMeasurement.voltage({ value = 0.11, unit = "V"})) + ) + + test.socket.zigbee:__queue_receive({ mock_device.id, ElectricalMeasurement.attributes.RMSCurrentPhC:build_test_attr_report(mock_device, 120) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("phaseC", capabilities.currentMeasurement.current({ value = 0.12, unit = "A"})) + ) + + end +) + +test.register_coroutine_test( + "CurrentSummationDelivered Report should be handled.", + function() + local current_time = os.time() - 60 * 16 + mock_device:set_field(LAST_REPORT_TIME, current_time) + + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.Divisor:build_test_attr_report(mock_device, 1000) }) + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.Multiplier:build_test_attr_report(mock_device, 1) }) + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 2700) }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 2700.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.powerConsumptionReport.powerConsumption({ + start = "1969-12-31T23:44:00Z", + ["end"] = "1969-12-31T23:59:59Z", + deltaEnergy = 0.0, + energy = 2700.0 + }) + ) + ) + end +) + +test.register_coroutine_test( + "CurrentSummationDelivered report should be handled without powerConsumptionReport because 15 min didn't pass since last report", + function() + local current_time = os.time() - 60 * 14 + mock_device:set_field(LAST_REPORT_TIME, current_time) + + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.Divisor:build_test_attr_report(mock_device, 1000) }) + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.Multiplier:build_test_attr_report(mock_device, 1) }) + test.socket.zigbee:__queue_receive({ mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 2700) }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 2700.0, unit = "Wh" })) + ) + end +) + +test.register_coroutine_test( + "lifecycle configure event should configure the device", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + expected_refresh_commands() + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + SimpleMetering.ID + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + ElectricalMeasurement.ID + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting( + mock_device, 1, 43200, 1 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting( + mock_device, 1, 43200, 1 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:configure_reporting( + mock_device, 5, 3600, 5 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhB:configure_reporting( + mock_device, 5, 3600, 5 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhC:configure_reporting( + mock_device, 5, 3600, 5 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltage:configure_reporting( + mock_device, 5, 3600, 5 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhB:configure_reporting( + mock_device, 5, 3600, 5 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhC:configure_reporting( + mock_device, 5, 3600, 5 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting( + mock_device, 5, 3600, 5 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhB:configure_reporting( + mock_device, 5, 3600, 5 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhC:configure_reporting( + mock_device, 5, 3600, 5 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting( + mock_device, 5, 3600, 1 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting( + mock_device, 5, 3600, 1 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.Divisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.Multiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACVoltageDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACVoltageMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACCurrentDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACCurrentMultiplier:read(mock_device) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.configure_reporting( + mock_device, + data_types.ClusterId(SimpleMetering.ID), + data_types.AttributeId(CurrentSummationReceived), + data_types.ZigbeeDataType(data_types.Uint48.ID), + 5, + 3600, + 1 + ) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests()