diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index bd0fe0df8c..ecb75e2a82 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -274,6 +274,9 @@ local matter_driver_template = { [capabilities.colorTemperature.ID] = { [capabilities.colorTemperature.commands.setColorTemperature.NAME] = capability_handlers.handle_set_color_temperature, }, + [capabilities.energyMeter.ID] = { + [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = capability_handlers.handle_reset_energy_meter, + }, [capabilities.fanMode.ID] = { [capabilities.fanMode.commands.setFanMode.NAME] = capability_handlers.handle_set_fan_mode }, diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua index 40b1f6a44c..756a402de6 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -284,6 +284,11 @@ function AttributeHandlers.energy_imported_factory(is_periodic_report) device, ib, capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME ) or 0 energy_imported_wh = energy_imported_wh + energy_meter_latest_state + else + -- the field containing the offset may be associated with a child device + local field_device = switch_utils.find_child(device, ib.endpoint_id) or device + local energy_meter_offset = field_device:get_field(fields.ENERGY_METER_OFFSET) or 0.0 + energy_imported_wh = energy_imported_wh - energy_meter_offset end device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = energy_imported_wh, unit = "Wh" })) switch_utils.report_power_consumption_to_st_energy(device, ib.endpoint_id, energy_imported_wh) diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua index 77927ef1d2..2c5241de3a 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua @@ -168,4 +168,25 @@ function CapabilityHandlers.handle_fan_speed_set_percent(driver, device, cmd) device:send(clusters.FanControl.attributes.PercentSetting:write(device, fan_ep, speed)) end -return CapabilityHandlers \ No newline at end of file + +-- [[ ENERGY METER CAPABILITY COMMANDS ]] -- + +--- +--- If a Cumulative Reporting device, this will store the most recent energy meter reading, and all subsequent reports will have this value subtracted +--- from the value reported. Matter, like Zigbee and unlike Z-Wave, does not provide a way to reset the value to zero, so this is an attempt at a workaround. +--- In the case it is a Periodic Reporting device, the reports do not need to be offset, so setting the current energy to 0.0 will do the same thing. +--- +function CapabilityHandlers.handle_reset_energy_meter(driver, device, cmd) + local energy_meter_latest_state = device:get_latest_state(cmd.component, capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME) or 0.0 + if energy_meter_latest_state ~= 0.0 then + device:emit_component_event(device.profile.components[cmd.component], capabilities.energyMeter.energy({value = 0.0, unit = "Wh"})) + -- note: field containing cumulative reports supported is only set on the parent device + local field_device = device:get_parent_device() or device + if field_device:get_field(fields.CUMULATIVE_REPORTS_SUPPORTED) then + local current_offset = device:get_field(fields.ENERGY_METER_OFFSET) or 0.0 + device:set_field(fields.ENERGY_METER_OFFSET, current_offset + energy_meter_latest_state, {persist=true}) + end + end +end + +return CapabilityHandlers diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index ddd554069a..f0fd0166b4 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -155,6 +155,7 @@ SwitchFields.profiling_data = { POWER_TOPOLOGY = "__power_topology", } +SwitchFields.ENERGY_METER_OFFSET = "__energy_meter_offset" SwitchFields.CUMULATIVE_REPORTS_SUPPORTED = "__cumulative_reports_supported" SwitchFields.LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" SwitchFields.MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 1e61f60c74..e3c3f8a100 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -76,7 +76,8 @@ function utils.mired_to_kelvin(value, minOrMax) end function utils.get_product_override_field(device, override_key) - if fields.vendor_overrides[device.manufacturer_info.vendor_id] + if device.manufacturer_info + and fields.vendor_overrides[device.manufacturer_info.vendor_id] and fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id] then return fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id][override_key] diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua index 3df62056ca..412c773540 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua @@ -161,8 +161,17 @@ local periodic_report_val_23 = { reactive_energy = 0 } +local mock_child = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("plug-level-energy-powerConsumption.yml"), + device_network_id = string.format("%s:%d", mock_device.id, 4), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 4) +}) + local function test_init() test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_child) + local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) for i, cluster in ipairs(subscribed_attributes) do if i > 1 then @@ -431,7 +440,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test profile change on init for Electrical Sensor device type", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 2, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 4, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) @@ -463,6 +471,189 @@ test.register_coroutine_test( { test_init = test_init_periodic } ) +test.register_coroutine_test( + "Test resetEnergyMeter command on parent and child for CumulativeEnergyImported", + function() + + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + + -- this block needs to run to set the requisite fields. It is tested on its own elsewhere + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 2, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 4, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 1, {uint32(2)})}) + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 3, {uint32(4)})}) + mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "nil 2", + profile = "plug-level-energy-powerConsumption", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 4) + }) + -- end of block + + -- Initial Parent Energy Report + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_19 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 19.0 + })) + ) + + -- Initial Child Energy Report + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 3, cumulative_report_val_19 + ) + } + ) + test.socket.capability:__expect_send( + mock_child:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) + ) + -- no powerConsumptionReport will be emitted now, since it has not been 15 minutes since the previous report (even though it was the parent). + + + test.wait_for_events() + test.mock_time.advance_time(1500) + + + -- Parent call to resetEnergyMeter + test.socket.capability:__queue_receive({mock_device.id, { capability = "energyMeter", component = "main", command = "resetEnergyMeter", args = {}}}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) + ) + -- Child call to resetEnergyMeter + test.socket.capability:__queue_receive({mock_child.id, { capability = "energyMeter", component = "main", command = "resetEnergyMeter", args = {}}}) + test.socket.capability:__expect_send( + mock_child:generate_test_message("main", capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) + ) + + test.wait_for_events() + + -- Second Child Energy Report + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 3, cumulative_report_val_39 + ) + } + ) + test.socket.capability:__expect_send( + mock_child:generate_test_message("main", capabilities.energyMeter.energy({ value = 20.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_child:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:40:00Z", + deltaEnergy = 0.0, + energy = 20.0 + })) + ) + + -- Second Parent Energy Report + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_39 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 20.0, unit = "Wh" })) + ) + -- no powerConsumptionReport will be emitted now, since it has not been 15 minutes since the previous report (even though it was the child). + end, + { test_init = test_init } +) + +test.register_coroutine_test( + "Test resetEnergyMeter command on device for PeriodicEnergyImported", + function() + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + test.socket.matter:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 23.0, unit="Wh"})) + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 23.0 + })) + ) + test.socket.matter:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 46.0, unit="Wh"})) + ) + + test.wait_for_events() + test.mock_time.advance_time(2000) + + test.socket.capability:__queue_receive({mock_device_periodic.id, { capability = "energyMeter", component = "main", command = "resetEnergyMeter", args = {}}}) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) + ) + + test.wait_for_events() + + test.socket.matter:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device_periodic, 1, cumulative_report_val_19 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 19.0, unit="Wh"})) + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:48:20Z", + deltaEnergy = -4.0, + energy = 19.0 + })) + ) + end, + { test_init = test_init_periodic } +) + test.register_message_test( "Set level command should send the appropriate commands", {