From bfabc0124720561bcb7159e60793faa953f2b712 Mon Sep 17 00:00:00 2001 From: Marcin Tyminski Date: Mon, 26 Jan 2026 14:26:19 +0100 Subject: [PATCH 1/2] add driver --- .../zigbee-power-meter/fingerprints.yml | 18 +- ...ower-energy-battery-consumption-report.yml | 37 ++ .../power-energy-consumption-report.yml | 35 ++ .../profiles/power-energy-current-voltage.yml | 48 +++ .../src/frient/EMIZB-151/can_handle.lua | 15 + .../src/frient/EMIZB-151/fingerprints.lua | 8 + .../src/frient/EMIZB-151/init.lua | 398 +++++++++++++++++ .../src/frient/fingerprints.lua | 5 +- .../zigbee-power-meter/src/frient/init.lua | 242 ++++++++++- .../zigbee-power-meter/src/frient/utils.lua | 21 + .../zigbee-power-meter/src/init.lua | 1 + .../zigbee-power-meter/src/sub_drivers.lua | 1 + ...ower_energy_battery_consumption_report.lua | 341 +++++++++++++++ ...frient_power_energy_consumption_report.lua | 304 +++++++++++++ ...st_frient_power_energy_current_voltage.lua | 399 ++++++++++++++++++ 15 files changed, 1855 insertions(+), 18 deletions(-) create mode 100644 drivers/SmartThings/zigbee-power-meter/profiles/power-energy-battery-consumption-report.yml create mode 100644 drivers/SmartThings/zigbee-power-meter/profiles/power-energy-consumption-report.yml create mode 100644 drivers/SmartThings/zigbee-power-meter/profiles/power-energy-current-voltage.yml create mode 100644 drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/init.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/frient/utils.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_battery_consumption_report.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_consumption_report.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_current_voltage.lua 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..607eaa17a3 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/init.lua @@ -0,0 +1,398 @@ +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 powerMeter_defaults = require "st.zigbee.defaults.powerMeter_defaults" +local energyMeter_defaults = require "st.zigbee.defaults.energyMeter_defaults" + +local data_types = require "st.zigbee.data_types" +local log = require "log" +local LAST_REPORT_TIME = "LAST_REPORT_TIME" +local SIMPLE_METERING_DEFAULT_DIVISOR = 1000 + +local ZIGBEE_POWER_METER_FINGERPRINTS = require("frient/EMIZB-151.fingerprints") + +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) + --device:add_monitored_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 + --- 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 divisor == 0 then + log.warn_with({ hub_logs = true }, "Metering scale divisor is 0; using 1 to avoid division by zero") + 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 + log.warn_with({ hub_logs = true }, "Invalid CurrentSummationDelivered value received; ignoring report") + 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 + log.warn_with({ hub_logs = true }, "Metering scale divisor is 0; using 1 to avoid division by zero") + divisor = 1 + end + + raw_value = raw_value * multiplier / divisor * 1000 + log.debug("CurrentSummationDelivered: raw=" .. tostring(value.value) .. ", multiplier=" .. tostring(multiplier) .. ", divisor=" .. tostring(divisor) .. ", final=" .. tostring(raw_value)) + 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() + log.trace("current_time: "..current_time) + local last_report_time = device:get_field(LAST_REPORT_TIME) or 0 + log.trace("last_report_time: "..last_report_time) + local next_report_time = last_report_time + 60 * 15 -- 15 mins, the minimum interval allowed between reports + log.trace("next_report_time: "..next_report_time) + if current_time < next_report_time then + log.trace("EXIT: ") + 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 + log.warn_with({ hub_logs = true }, "Invalid CurrentSummationReceived value received; ignoring report") + 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 + log.warn_with({ hub_logs = true }, "Metering scale divisor is 0; using 1 to avoid division by zero") + divisor = 1 + end + + raw_value = raw_value * multiplier / divisor * 1000 + log.debug("CurrentSummationReceived: raw=" .. tostring(value.value) .. ", multiplier=" .. tostring(multiplier) .. ", divisor=" .. tostring(divisor) .. ", final=" .. tostring(raw_value)) + 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 + log.debug("Setting AC voltage multiplier: " .. tostring(raw_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 + log.debug("Setting AC voltage divisor: " .. tostring(raw_value)) + 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 + log.debug("Setting AC current multiplier: " .. tostring(raw_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 + log.debug("Setting AC current divisor: " .. tostring(raw_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 + log.warn_with({ hub_logs = true }, "Power scale divisor is 0; using 1 to avoid division by zero") + 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 + log.warn_with({ hub_logs = true }, "Voltage scale divisor is 0; using 1 to avoid division by zero") + 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 + log.warn_with({ hub_logs = true }, "Current scale divisor is 0; using 1 to avoid division by zero") + 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 + log.debug_with( + { hub_logs = true }, + string.format( + "Ignoring divisor report (mfg_specific=%s, type=%s, value=%s)", + tostring(is_mfg_specific), + has_expected_type and "Uint24" or tostring(divisor and divisor.NAME or "nil"), + tostring(divisor and divisor.value) + ) + ) + return + end + + local raw_value = divisor.value + log.info_with({ hub_logs = true }, "Received Simple Metering divisor: " .. tostring(raw_value)) + + if raw_value == 0 then + log.warn_with({ hub_logs = true }, "Simple metering divisor is 0; using 1 to avoid division by zero") + 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 + log.info_with({ hub_logs = true }, "Received Simple Metering multiplier: " .. tostring(raw_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.ACPowerDivisor.ID] = powerMeter_defaults.electrical_measurement_divisor_handler, + --[ElectricalMeasurement.attributes.ACPowerMultiplier.ID] = powerMeter_defaults.electrical_measurement_multiplier_handler, + [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..e3e237d06c 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua @@ -1,28 +1,246 @@ -- 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" +-- KK: local energyMeter_defaults = require "st.zigbee.defaults.energyMeter_defaults" -local do_configure = function(self, device) - device:refresh() - device:configure() +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 is_frient_power_meter = function(opts, driver, device) + for _, fingerprint in ipairs(ZIGBEE_POWER_METER_FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return true + end + end + + return false 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}) + for _, fingerprint in ipairs(ZIGBEE_POWER_METER_FINGERPRINTS) do + -- KK: added additional condition + 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) + -- KK removed: device:add_monitored_attribute(attribute) + end +end + +local do_refresh = function(self, device) + device:refresh() + -- KK: above device:refresh() already sends below commands + -- KK: device:send(SimpleMetering.attributes.CurrentSummationDelivered:read(device)) + -- KK: device:send(SimpleMetering.attributes.InstantaneousDemand:read(device)) + 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 + -- KK: added additional condition + if device:get_model() == fingerprint.model and fingerprint.preferences then + local pulseConfiguration = tonumber(device.preferences.pulseConfiguration) or 1000 + -- KK: log.debug("Writing pulse configuration to: " .. pulseConfiguration) + 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 + -- KK: log.debug("Writing initial current summation to: " .. 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 + + -- 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 function info_changed(driver, device, event, args) + log.trace("Configuring sensor:"..event) + 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) + -- KK: log.debug("Configuring pulseConfiguration: "..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) + -- KK: log.debug("Configuring currentSummation: "..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) + -- KK: I refactored it a bit + local new_divisor = SIMPLE_METERING_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, raw_value, { 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 + log.trace("raw_value (1): "..raw_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 + log.warn_with({ hub_logs = true }, "Invalid CurrentSummationDelivered value received; ignoring report") + return + end + + log.trace("raw_value (before * multiplier/divisor): "..raw_value) + log.trace("multiplier: "..multiplier) + log.trace("divisor: "..divisor) + raw_value = (raw_value * multiplier) / divisor + log.trace("raw_value * multiplier / divisor is "..raw_value) + + local offset = device:get_field(zigbee_constants.ENERGY_METER_OFFSET) or 0 + log.trace("offset: "..offset) + 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 }) + log.trace("offset 0 was set ") + end + raw_value = raw_value - offset + log.trace("raw_value - offset "..raw_value) + raw_value = raw_value * 1000 -- the unit of these values should be 'Wh' + log.trace("raw_value = raw_value * 1000: "..raw_value) + + 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() + log.trace("current_time: "..current_time) + local last_report_time = device:get_field(LAST_REPORT_TIME) or 0 + log.trace("last_report_time: "..last_report_time) + local next_report_time = last_report_time + 60 * 15 -- 15 mins, the minimum interval allowed between reports + log.trace("next_report_time: "..next_report_time) + if current_time < next_report_time then + log.trace("EXIT: ") + 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), - doConfigure = do_configure, - }, + NAME = "frient power meter handler", + lifecycle_handlers = { + 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..95f1f033dc --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_battery_consumption_report.lua @@ -0,0 +1,341 @@ +-- 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 utils = require "frient.utils" + +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..e9ecc46589 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_consumption_report.lua @@ -0,0 +1,304 @@ +-- 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 utils = require "frient.utils" + +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..a26e2ab386 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_current_voltage.lua @@ -0,0 +1,399 @@ +-- 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 utils = require "frient.utils" + +local DEVELCO_MANUFACTURER_CODE = 0x1015 +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() From 115fa8e312ae6a8ecca668887926ba0037ec6c74 Mon Sep 17 00:00:00 2001 From: Marcin Tyminski Date: Mon, 26 Jan 2026 14:53:41 +0100 Subject: [PATCH 2/2] removed unused variables --- .../src/frient/EMIZB-151/init.lua | 578 ++++++++---------- .../zigbee-power-meter/src/frient/init.lua | 340 +++++------ ...ower_energy_battery_consumption_report.lua | 1 - ...frient_power_energy_consumption_report.lua | 1 - ...st_frient_power_energy_current_voltage.lua | 3 - 5 files changed, 422 insertions(+), 501 deletions(-) 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 index 607eaa17a3..5c348ed744 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zigbee_constants = require "st.zigbee.constants" local capabilities = require "st.capabilities" @@ -6,16 +9,10 @@ local SimpleMetering = clusters.SimpleMetering local ElectricalMeasurement = clusters.ElectricalMeasurement local utils = require "frient.utils" -local powerMeter_defaults = require "st.zigbee.defaults.powerMeter_defaults" -local energyMeter_defaults = require "st.zigbee.defaults.energyMeter_defaults" - local data_types = require "st.zigbee.data_types" -local log = require "log" local LAST_REPORT_TIME = "LAST_REPORT_TIME" local SIMPLE_METERING_DEFAULT_DIVISOR = 1000 -local ZIGBEE_POWER_METER_FINGERPRINTS = require("frient/EMIZB-151.fingerprints") - 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" @@ -24,375 +21,342 @@ zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_DIVISOR_KEY = "_electrical_me 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 - } + { + 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) - --device:add_monitored_attribute(attribute) - end + 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)) + 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 - --- 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 + 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 - log.warn_with({ hub_logs = true }, "Metering scale divisor is 0; using 1 to avoid division by zero") - divisor = 1 - end + if divisor == 0 then + divisor = 1 + end - raw_value = raw_value * multiplier / divisor * 1000 + 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" })) + -- 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 - log.warn_with({ hub_logs = true }, "Invalid CurrentSummationDelivered value received; ignoring report") - 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 - log.warn_with({ hub_logs = true }, "Metering scale divisor is 0; using 1 to avoid division by zero") - divisor = 1 - end - - raw_value = raw_value * multiplier / divisor * 1000 - log.debug("CurrentSummationDelivered: raw=" .. tostring(value.value) .. ", multiplier=" .. tostring(multiplier) .. ", divisor=" .. tostring(divisor) .. ", final=" .. tostring(raw_value)) - 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() - log.trace("current_time: "..current_time) - local last_report_time = device:get_field(LAST_REPORT_TIME) or 0 - log.trace("last_report_time: "..last_report_time) - local next_report_time = last_report_time + 60 * 15 -- 15 mins, the minimum interval allowed between reports - log.trace("next_report_time: "..next_report_time) - if current_time < next_report_time then - log.trace("EXIT: ") - 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 }) + 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 + local raw_value = value.value - -- Handle potential overflow values - if raw_value < 0 or raw_value >= 0xFFFFFFFFFFFF then - log.warn_with({ hub_logs = true }, "Invalid CurrentSummationReceived value received; ignoring report") - return - end + -- 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 + 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 - log.warn_with({ hub_logs = true }, "Metering scale divisor is 0; using 1 to avoid division by zero") - divisor = 1 - end + if divisor == 0 then + divisor = 1 + end - raw_value = raw_value * multiplier / divisor * 1000 - log.debug("CurrentSummationReceived: raw=" .. tostring(value.value) .. ", multiplier=" .. tostring(multiplier) .. ", divisor=" .. tostring(divisor) .. ", final=" .. tostring(raw_value)) - device:emit_component_event(device.profile.components['production'], capabilities.energyMeter.energy({ value = raw_value, unit = "Wh" })) + 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 - log.debug("Setting AC voltage multiplier: " .. tostring(raw_value)) - device:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_MULTIPLIER_KEY, raw_value, { persist = true }) + 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 - log.debug("Setting AC voltage divisor: " .. tostring(raw_value)) - device:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_DIVISOR_KEY, raw_value, { persist = true }) + 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 - log.debug("Setting AC current multiplier: " .. tostring(raw_value)) - device:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_MULTIPLIER_KEY, raw_value, { persist = true }) + 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 - log.debug("Setting AC current divisor: " .. tostring(raw_value)) - device:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_DIVISOR_KEY, raw_value, { persist = true }) + 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 + 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 - log.warn_with({ hub_logs = true }, "Power scale divisor is 0; using 1 to avoid division by zero") - divisor = 1 - end + if divisor == 0 then + divisor = 1 + end - raw_value = raw_value * multiplier / divisor + raw_value = raw_value * multiplier / divisor - device:emit_component_event(device.profile.components[component], capabilities.powerMeter.power({ value = raw_value, unit = "W" })) - end + device:emit_component_event(device.profile.components[component], capabilities.powerMeter.power({ value = raw_value, unit = "W" })) + end - return handler + 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 + 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 - log.warn_with({ hub_logs = true }, "Voltage scale divisor is 0; using 1 to avoid division by zero") - divisor = 1 - end + if divisor == 0 then + divisor = 1 + end - raw_value = raw_value * multiplier / divisor + raw_value = raw_value * multiplier / divisor - device:emit_component_event(device.profile.components[component], capabilities.voltageMeasurement.voltage({ value = raw_value, unit = "V" })) - end + device:emit_component_event(device.profile.components[component], capabilities.voltageMeasurement.voltage({ value = raw_value, unit = "V" })) + end - return handler + 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 + 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 - log.warn_with({ hub_logs = true }, "Current scale divisor is 0; using 1 to avoid division by zero") - divisor = 1 - end + if divisor == 0 then + divisor = 1 + end - raw_value = raw_value * multiplier / divisor + raw_value = raw_value * multiplier / divisor - device:emit_component_event(device.profile.components[component], capabilities.currentMeasurement.current({ value = raw_value, unit = "A" })) - end + device:emit_component_event(device.profile.components[component], capabilities.currentMeasurement.current({ value = raw_value, unit = "A" })) + end - return handler + 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 - log.debug_with( - { hub_logs = true }, - string.format( - "Ignoring divisor report (mfg_specific=%s, type=%s, value=%s)", - tostring(is_mfg_specific), - has_expected_type and "Uint24" or tostring(divisor and divisor.NAME or "nil"), - tostring(divisor and divisor.value) - ) - ) - return - end + 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 - local raw_value = divisor.value - log.info_with({ hub_logs = true }, "Received Simple Metering divisor: " .. tostring(raw_value)) + if is_mfg_specific or not has_expected_type then + return + end - if raw_value == 0 then - log.warn_with({ hub_logs = true }, "Simple metering divisor is 0; using 1 to avoid division by zero") - raw_value = 1 - end + local raw_value = divisor.value - device:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, raw_value, { persist = true }) + 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 - log.info_with({ hub_logs = true }, "Received Simple Metering multiplier: " .. tostring(raw_value)) - device:set_field(zigbee_constants.SIMPLE_METERING_MULTIPLIER_KEY, raw_value, { persist = true }) - end + 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.ACPowerDivisor.ID] = powerMeter_defaults.electrical_measurement_divisor_handler, - --[ElectricalMeasurement.attributes.ACPowerMultiplier.ID] = powerMeter_defaults.electrical_measurement_multiplier_handler, - [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") - } - } + NAME = "EMIZB-151", + lifecycle_handlers = { + init = device_init, + doConfigure = do_configure + }, + zigbee_handlers = { + cluster = { }, - can_handle = require("frient/EMIZB-151.can_handle") + 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/init.lua b/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua index e3e237d06c..f47b156e87 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua @@ -13,8 +13,6 @@ local utils = require "frient.utils" local LAST_REPORT_TIME = "LAST_REPORT_TIME" --- KK: local energyMeter_defaults = require "st.zigbee.defaults.energyMeter_defaults" - local data_types = require "st.zigbee.data_types" local log = require "log" @@ -24,223 +22,187 @@ 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 - } + { + 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 is_frient_power_meter = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_POWER_METER_FINGERPRINTS) do - if device:get_model() == fingerprint.model then - return true - end - end - - return false -end - local device_init = function(self, device) - for _, fingerprint in ipairs(ZIGBEE_POWER_METER_FINGERPRINTS) do - -- KK: added additional condition - 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) - -- KK removed: device:add_monitored_attribute(attribute) + 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() - -- KK: above device:refresh() already sends below commands - -- KK: device:send(SimpleMetering.attributes.CurrentSummationDelivered:read(device)) - -- KK: device:send(SimpleMetering.attributes.InstantaneousDemand:read(device)) - if device:supports_capability(capabilities.battery) then - device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) - end + 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 - -- KK: added additional condition - if device:get_model() == fingerprint.model and fingerprint.preferences then - local pulseConfiguration = tonumber(device.preferences.pulseConfiguration) or 1000 - -- KK: log.debug("Writing pulse configuration to: " .. pulseConfiguration) - 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 - -- KK: log.debug("Writing initial current summation to: " .. currentSummation) - device:send(cluster_base.write_manufacturer_specific_attribute(device, SimpleMetering.ID, 0x0301, DEVELCO_MANUFACTURER_CODE, data_types.Uint48, currentSummation):to_endpoint(0x02)) - end + 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)) + -- 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) + device.thread:call_with_delay(5, function() + do_refresh(self, device) + end) end local function info_changed(driver, device, event, args) - log.trace("Configuring sensor:"..event) - 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) - -- KK: log.debug("Configuring pulseConfiguration: "..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) - -- KK: log.debug("Configuring currentSummation: "..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 + 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 - device.thread:call_with_delay(5, function() - do_refresh(driver, device) - 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) - -- KK: I refactored it a bit - local new_divisor = SIMPLE_METERING_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, raw_value, { persist = true }) + 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" })) + 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 - log.trace("raw_value (1): "..raw_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 - log.warn_with({ hub_logs = true }, "Invalid CurrentSummationDelivered value received; ignoring report") - return - end - - log.trace("raw_value (before * multiplier/divisor): "..raw_value) - log.trace("multiplier: "..multiplier) - log.trace("divisor: "..divisor) - raw_value = (raw_value * multiplier) / divisor - log.trace("raw_value * multiplier / divisor is "..raw_value) - - local offset = device:get_field(zigbee_constants.ENERGY_METER_OFFSET) or 0 - log.trace("offset: "..offset) - 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 }) - log.trace("offset 0 was set ") - end - raw_value = raw_value - offset - log.trace("raw_value - offset "..raw_value) - raw_value = raw_value * 1000 -- the unit of these values should be 'Wh' - log.trace("raw_value = raw_value * 1000: "..raw_value) - - 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() - log.trace("current_time: "..current_time) - local last_report_time = device:get_field(LAST_REPORT_TIME) or 0 - log.trace("last_report_time: "..last_report_time) - local next_report_time = last_report_time + 60 * 15 -- 15 mins, the minimum interval allowed between reports - log.trace("next_report_time: "..next_report_time) - if current_time < next_report_time then - log.trace("EXIT: ") - 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 }) + 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 = 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") + NAME = "frient power meter handler", + lifecycle_handlers = { + 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/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 index 95f1f033dc..ae503638c8 100644 --- 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 @@ -23,7 +23,6 @@ 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 utils = require "frient.utils" local DEVELCO_MANUFACTURER_CODE = 0x1015 local LAST_REPORT_TIME = "LAST_REPORT_TIME" 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 index e9ecc46589..17c0ce071d 100644 --- 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 @@ -23,7 +23,6 @@ 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 utils = require "frient.utils" local DEVELCO_MANUFACTURER_CODE = 0x1015 local LAST_REPORT_TIME = "LAST_REPORT_TIME" 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 index a26e2ab386..7dd7fbc336 100644 --- 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 @@ -17,15 +17,12 @@ 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 utils = require "frient.utils" -local DEVELCO_MANUFACTURER_CODE = 0x1015 local CurrentSummationReceived = 0x0001 local LAST_REPORT_TIME = "LAST_REPORT_TIME"