From b07b833a4e1a081358c9f6a46ea308d46b95d6d5 Mon Sep 17 00:00:00 2001 From: donghoon-ryu Date: Fri, 13 Dec 2024 20:47:31 +0900 Subject: [PATCH] Code sharing for 2 switch and 4 switch --- .../matter-switch/profiles/light-button.yml | 14 - ...> light-power-energy-powerConsumption.yml} | 8 +- .../src/aqara-light-switch-h2/init.lua | 620 ------------------ .../SmartThings/matter-switch/src/init.lua | 51 +- .../src/test/test_aqara_light_switch_h2.lua | 295 --------- .../src/test/test_electrical_sensor.lua | 2 + 6 files changed, 45 insertions(+), 945 deletions(-) delete mode 100644 drivers/SmartThings/matter-switch/profiles/light-button.yml rename drivers/SmartThings/matter-switch/profiles/{light-button-electricalMeasurement.yml => light-power-energy-powerConsumption.yml} (69%) delete mode 100644 drivers/SmartThings/matter-switch/src/aqara-light-switch-h2/init.lua delete mode 100755 drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua diff --git a/drivers/SmartThings/matter-switch/profiles/light-button.yml b/drivers/SmartThings/matter-switch/profiles/light-button.yml deleted file mode 100644 index 2eb0b4f871..0000000000 --- a/drivers/SmartThings/matter-switch/profiles/light-button.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: light-button -components: - - id: main - capabilities: - - id: switch - version: 1 - - id: button - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: Light diff --git a/drivers/SmartThings/matter-switch/profiles/light-button-electricalMeasurement.yml b/drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml similarity index 69% rename from drivers/SmartThings/matter-switch/profiles/light-button-electricalMeasurement.yml rename to drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml index e9533562bd..a72f39d17b 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-button-electricalMeasurement.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml @@ -1,4 +1,4 @@ -name: light-button-electricalMeasurement +name: light-power-energy-powerConsumption components: - id: main capabilities: @@ -16,10 +16,4 @@ components: version: 1 categories: - name: Light - - id: button - capabilities: - - id: button - version: 1 - categories: - - name: Button diff --git a/drivers/SmartThings/matter-switch/src/aqara-light-switch-h2/init.lua b/drivers/SmartThings/matter-switch/src/aqara-light-switch-h2/init.lua deleted file mode 100644 index b6e9ac9ee3..0000000000 --- a/drivers/SmartThings/matter-switch/src/aqara-light-switch-h2/init.lua +++ /dev/null @@ -1,620 +0,0 @@ --- Copyright 2022 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 capabilities = require "st.capabilities" -local log = require "log" -local clusters = require "st.matter.clusters" -local device_lib = require "st.device" - --- COMPONENT_TO_ENDPOINT_MAP is here only to preserve the endpoint mapping for --- devices that were joined to this driver as MCD devices before the transition --- to join all matter-switch devices as parent-child. This value will only exist --- in the device table for devices that joined prior to this transition, and it --- will not be set for new devices. -local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -local IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" -local SECOND_SWITCH_ID = "__second_switch_id" -local SECOND_SWITCH_ENDPOINT = "__second_switch_endpoint" -local SECOND_BUTTON_ENDPOINT = "__second_button_endpoint" - -local ON_OFF_LIGHT_DEVICE_TYPE_ID = 0x0100 -local GENERIC_SWITCH_ID = 0x000F -local ELECTRICAL_SENSOR_ID = 0x0510 -local device_type_profile_map = { - [ON_OFF_LIGHT_DEVICE_TYPE_ID] = "light-button", - [GENERIC_SWITCH_ID] = "button" -} - -local device_type_id_map = { - ON_OFF_LIGHT_DEVICE_TYPE_ID, - GENERIC_SWITCH_ID, - ELECTRICAL_SENSOR_ID -} - -local device_type_attribute_map = { - [ON_OFF_LIGHT_DEVICE_TYPE_ID] = { - clusters.OnOff.attributes.OnOff - }, - [GENERIC_SWITCH_ID] = { - clusters.Switch.events.InitialPress - }, - [ELECTRICAL_SENSOR_ID] = { - clusters.ElectricalPowerMeasurement.attributes.ActivePower, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported - } -} - -local CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" -local FIRST_EXPORT_REPORT_TIMESTAMP = "__first_export_report_timestamp" -local EXPORT_POLL_TIMER_SETTING_ATTEMPTED = "__export_poll_timer_setting_attempted" -local EXPORT_REPORT_TIMEOUT = "__export_report_timeout" -local TOTAL_EXPORTED_ENERGY = "__total_exported_energy" -local LAST_EXPORTED_REPORT_TIMESTAMP = "__last_exported_report_timestamp" -local RECURRING_EXPORT_REPORT_POLL_TIMER = "__recurring_export_report_poll_timer" -local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds -local SUBSCRIPTION_REPORT_OCCURRED = "__subscription_report_occurred" -local CONVERSION_CONST_MILLIWATT_TO_WATT = 1000 -- A milliwatt is 1/1000th of a watt - -local embedded_cluster_utils = require "embedded-cluster-utils" - --- Include driver-side definitions when lua libs api version is < 11 -local version = require "version" -if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" -end - --- Return an ISO-8061 timestamp in UTC -local function iso8061Timestamp(time) - return os.date("!%Y-%m-%dT%H:%M:%SZ", time) -end - -local function delete_export_poll_schedule(device) - local export_poll_timer = device:get_field(RECURRING_EXPORT_REPORT_POLL_TIMER) - if export_poll_timer then - device.thread:cancel_timer(export_poll_timer) - device:set_field(RECURRING_EXPORT_REPORT_POLL_TIMER, nil) - device:set_field(EXPORT_POLL_TIMER_SETTING_ATTEMPTED, nil) - end -end - -local function send_export_poll_report(device, latest_total_exported_energy_wh) - local current_time = os.time() - local last_time = device:get_field(LAST_EXPORTED_REPORT_TIMESTAMP) or 0 - device:set_field(LAST_EXPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) - - -- Calculate the energy delta between reports - local energy_delta_wh = 0.0 - local previous_exported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, - capabilities.powerConsumptionReport.powerConsumption.NAME) - if previous_exported_report and previous_exported_report.energy then - energy_delta_wh = math.max(latest_total_exported_energy_wh - previous_exported_report.energy, 0.0) - end - - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ - start = iso8061Timestamp(last_time), - ["end"] = iso8061Timestamp(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_exported_energy_wh - })) -end - -local function create_poll_report_schedule(device) - local export_timer = device.thread:call_on_schedule( - device:get_field(EXPORT_REPORT_TIMEOUT), - send_export_poll_report(device, device:get_field(TOTAL_EXPORTED_ENERGY)), - "polling_export_report_schedule_timer" - ) - device:set_field(RECURRING_EXPORT_REPORT_POLL_TIMER, export_timer) -end - -local function set_poll_report_timer_and_schedule(device, is_cumulative_report) - local cumul_eps = embedded_cluster_utils.get_endpoints(device, - clusters.ElectricalEnergyMeasurement.ID, - {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY - | clusters.ElectricalEnergyMeasurement.types.Feature.IMPORTED_ENERGY}) - if #cumul_eps == 0 then - device:set_field(CUMULATIVE_REPORTS_NOT_SUPPORTED, true) - end - if #cumul_eps > 0 and not is_cumulative_report then - return - elseif not device:get_field(SUBSCRIPTION_REPORT_OCCURRED) then - device:set_field(SUBSCRIPTION_REPORT_OCCURRED, true) - elseif not device:get_field(FIRST_EXPORT_REPORT_TIMESTAMP) then - device:set_field(FIRST_EXPORT_REPORT_TIMESTAMP, os.time()) - else - local first_timestamp = device:get_field(FIRST_EXPORT_REPORT_TIMESTAMP) - local second_timestamp = os.time() - local report_interval_secs = second_timestamp - first_timestamp - device:set_field(EXPORT_REPORT_TIMEOUT, math.max(report_interval_secs, MINIMUM_ST_ENERGY_REPORT_INTERVAL)) - -- the poll schedule is only needed for devices that support powerConsumption - if device:supports_capability(capabilities.powerConsumptionReport) then - create_poll_report_schedule(device) - end - device:set_field(EXPORT_POLL_TIMER_SETTING_ATTEMPTED, true) - end -end - --- used in unit testing, since device.profile.id and args.old_st_store.profile.id are always the same --- and this is to avoid the crash of the test case that occurs when try_update_metadata is performed in the device_init stage. -local TEST_CONFIGURE = "__test_configure" -local DEFERRED_CONFIGURE = "__DEFERRED_CONFIGURE" - --- These are essentially storing the supported features of a given endpoint --- TODO: add an is_feature_supported_for_endpoint function to matter.device that takes an endpoint -local INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease) - -local function is_aqara_light_switch_h2(opts, driver, device) - local name = string.format("%s", device.manufacturer_info.product_name) - if device.network_type == device_lib.NETWORK_TYPE_MATTER and - string.find(name, "Aqara Light Switch H2") then - return true - end - return false -end - -local function get_field_for_endpoint(device, field, endpoint) - return device:get_field(string.format("%s_%d", field, endpoint)) -end - -local function set_field_for_endpoint(device, field, endpoint, value, additional_params) - device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) -end - -local function get_first_non_zero_endpoint(endpoints) - for _,ep in ipairs(endpoints) do - if ep ~= 0 then -- 0 is the matter RootNode endpoint - return ep - end - end - return nil -end - ---- find_default_endpoint helper function to handle situations where ---- device does not have endpoint ids in sequential order from 1 ---- In this case the function returns the lowest endpoint value that isn't 0 ---- and supports the OnOff or Switch cluster. This is done to bypass the ---- BRIDGED_NODE_DEVICE_TYPE on bridged devices. -local function find_default_endpoint(device) - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - table.sort(switch_eps) - - -- Return the first switch endpoint as the default endpoint if no button endpoints are available - if #switch_eps > 0 then - return get_first_non_zero_endpoint(switch_eps) - end - - device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) - return device.MATTER_DEFAULT_ENDPOINT -end - -local function assign_child_profile(device, child_ep) - local profile - - for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == child_ep then - -- Some devices report multiple device types which are a subset of - -- a superset device type (For example, Dimmable Light is a superset of - -- On/Off light). This mostly applies to the four light types, so we will want - -- to match the profile for the superset device type. This can be done by - -- matching to the device type with the highest ID - local id = 0 - for _, dt in ipairs(ep.device_types) do - id = math.max(id, dt.device_type_id) - end - profile = device_type_profile_map[id] - break - end - end - -- default to "switch-binary" if no profile is found - return profile or "switch-binary" -end - -local function do_configure(driver, device) -end - -local function configure_buttons(device) - if device.network_type ~= device_lib.NETWORK_TYPE_CHILD then - local MS = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - device.log.debug(#MS.." momentary switch endpoints") - for _, ep in ipairs(MS) do - -- device only supports momentary switch, no release events - device.log.debug("configuring for press event only") - set_field_for_endpoint(device, INITIAL_PRESS_ONLY, ep, true, {persist = true}) - if device:get_field(TEST_CONFIGURE) then - if _ == 1 then - device:emit_event_for_endpoint(ep, capabilities.button.supportedButtonValues({"pushed"}, {state_change = false}, {visibility = {displayed = false}})) - end - else - local sbe = device:get_field(SECOND_BUTTON_ENDPOINT) - if ep == sbe then - local sse = device:get_field(SECOND_SWITCH_ENDPOINT) - local ssi = device:get_field(SECOND_SWITCH_ID) - local child_list = device:get_child_list() - for _, child in pairs(child_list) do - if child.id == ssi then - device:emit_event_for_endpoint(sse, capabilities.button.supportedButtonValues({"pushed"}, {state_change = false}, {visibility = {displayed = false}})) - break - end - end - else - device:emit_event_for_endpoint(ep, capabilities.button.supportedButtonValues({"pushed"}, {state_change = false}, {visibility = {displayed = false}})) - end - end - end - end -end - -local function find_child(parent, ep_id) - return parent:get_child_by_parent_assigned_key(string.format("%d", ep_id)) -end - --- Since EDGE_CHILD supports only one component, the button cannot be processed as a component in card2 with two endpoints(switch and button). --- In other words, the profile must be specified as the button capability of the main component. --- Therefore, the following routine is required to change the event generated at the second button endpoint so that it can be --- processed at the main component of the second switch. -local function save_second_switch_id(device) - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - table.sort(switch_eps) - table.sort(button_eps) - - local component_map = {} - local current_component_number = 3 - for _, ep in ipairs(switch_eps) do - if _ == 1 then - component_map["main"] = ep - elseif _ == 2 then - -- Save the second switch endpoint to use the second button in the main component of the second switch. - device:set_field(SECOND_SWITCH_ENDPOINT, ep) - end - end - - for _, ep in ipairs(button_eps) do - if _ == 1 then - -- To use the component name of the first button as button - component_map["button"] = ep - elseif _ == 3 then - -- Save the second button endpoint to use the second button in the main component of the second switch. - component_map["button2"] = ep - device:set_field(SECOND_BUTTON_ENDPOINT, ep) - else - component_map[string.format("button%d", current_component_number)] = ep - current_component_number = current_component_number + 1 - end - end - - local sse = device:get_field(SECOND_SWITCH_ENDPOINT) - local child_list = device:get_child_list() - for _, child in pairs(child_list) do - for k, v in pairs(child) do - if k == "st_store" then - for k1, v1 in pairs(v) do - if string.find(k1, "parent_assigned_child_key") and v1 == string.format("%d", sse) then - device:set_field(SECOND_SWITCH_ID, child.id) - device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) - break - end - end - break - end - end - end -end - -local function initialize_switch(driver, device) - -- Aqara Light Switch H2 has the following device types for each endpoint in 4 physical switches. - -- The device type of switch is the on/off light(0x100) and the device type of button is the generic switch(0xF) - -- Card 1: switch (ep1, main component), button (ep4, button component, first button) - -- Card 2: switch (ep2, EDGE_CHILD), button (ep6, second button) - -- Card 3: button (ep5, EDGE_CHILD, third button) - -- Card 4: button (ep7, EDGE_CHILD, fourth button) - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - table.sort(switch_eps) - table.sort(button_eps) - - local profile_name - local num_switch_server_eps = 0 - local main_endpoint = device.MATTER_DEFAULT_ENDPOINT - - -- If both switch and button endpoints are present, check the device type on the main switch endpoint. - -- If it is not a supported device type, return the first light endpoint as the default endpoint. - if #switch_eps > 0 and #button_eps > 0 then - main_endpoint = get_first_non_zero_endpoint(switch_eps) - profile_name = "light-button-electricalMeasurement" - device:try_update_metadata({ profile = profile_name }) - end - - -- If switch endpoints are present, the first switch endpoint will be the main endpoint. - -- And other endpoints will be EDGE_CHILD devices. - for _, ep in ipairs(switch_eps) do - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - num_switch_server_eps = num_switch_server_eps + 1 - if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint - local name = string.format("%s %d", device.label, num_switch_server_eps) - local child_profile = assign_child_profile(device, ep) - driver:try_create_device( - { - type = "EDGE_CHILD", - label = name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%d", ep), - vendor_provided_label = name - } - ) - end - end - end - - for _, ep in ipairs(button_eps) do - -- the index of first button endpoint is 1 - -- the index of second button endpoint is 3 - -- the index of third button endpoint is 2 - -- the index of fourth button endpoint is 4 - if _ ~= 1 and _ ~= 3 then - num_switch_server_eps = num_switch_server_eps + 1 - if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint - local name = string.format("%s %d", device.label, num_switch_server_eps) - local child_profile = assign_child_profile(device, ep) - driver:try_create_device( - { - type = "EDGE_CHILD", - label = name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%d", ep), - vendor_provided_label = name - } - ) - end - end - end - - -- If the device is a parent child device, then set the find_child function on init. - -- This is persisted because initialize_switch is only run once, but find_child function should be set - -- on each driver init. - device:set_field(IS_PARENT_CHILD_DEVICE, true, {persist = true}) - device:set_field(DEFERRED_CONFIGURE, true) -end - -local function component_to_endpoint(device, component) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} - if map[component] then - return map[component] - end - return find_default_endpoint(device) -end - -local function endpoint_to_component(device, ep) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} - for component, endpoint in pairs(map) do - if endpoint == ep then - return component - end - end - return "main" -end - -local function device_init(driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER then - -- initialize_switch will create parent-child devices as needed for multi-switch devices. - -- However, we want to maintain support for existing MCD devices, so do not initialize - -- device if it has already been previously initialized as an MCD device. - -- when unit testing, call initialize_switch elsewhere - if not device:get_field(TEST_CONFIGURE) then - if not device:get_field(COMPONENT_TO_ENDPOINT_MAP) then - -- create child devices as needed for multi-switch devices - initialize_switch(driver, device) - end - save_second_switch_id(device) - end - device:set_component_to_endpoint_fn(component_to_endpoint) - device:set_endpoint_to_component_fn(endpoint_to_component) - if device:get_field(IS_PARENT_CHILD_DEVICE) == true then - device:set_find_child(find_child) - end - - for _, id in ipairs(device_type_id_map) do - for _, attr in pairs(device_type_attribute_map[id] or {}) do - if id == GENERIC_SWITCH_ID then - device:add_subscribed_event(attr) - else - device:add_subscribed_attribute(attr) - end - end - end - - configure_buttons(device) - device:subscribe() - end -end - -local function device_removed(driver, device) - log.info("device removed") - delete_export_poll_schedule(device) -end - -local function handle_refresh(driver, device, cmd) - --Note: no endpoint specified indicates a wildcard endpoint - local req = clusters.OnOff.attributes.OnOff:read(device) - device:send(req) -end - --- Fallback handler for responses that dont have their own handler -local function matter_handler(driver, device, response_block) - log.info(string.format("Fallback handler for %s", response_block)) -end - -local function on_off_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on()) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) - end -end - ---TODO setup configure handler to read this attribute. -local function cumul_energy_exported_handler(driver, device, ib, response) - if ib.data.elements.energy then - local watt_hour_value = ib.data.elements.energy.value / CONVERSION_CONST_MILLIWATT_TO_WATT - device:set_field(TOTAL_EXPORTED_ENERGY, watt_hour_value) - device:emit_event(capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) - end -end - -local function per_energy_exported_handler(driver, device, ib, response) - if ib.data.elements.energy then - local watt_hour_value = ib.data.elements.energy.value / CONVERSION_CONST_MILLIWATT_TO_WATT - local latest_energy_report = device:get_field(TOTAL_EXPORTED_ENERGY) or 0 - local summed_energy_report = latest_energy_report + watt_hour_value - device:set_field(TOTAL_EXPORTED_ENERGY, summed_energy_report) - end -end - -local function energy_report_handler_factory(is_cumulative_report) - return function(driver, device, ib, response) - if not device:get_field(EXPORT_POLL_TIMER_SETTING_ATTEMPTED) then - set_poll_report_timer_and_schedule(device, is_cumulative_report) - end - if is_cumulative_report then - cumul_energy_exported_handler(driver, device, ib, response) - elseif device:get_field(CUMULATIVE_REPORTS_NOT_SUPPORTED) then - per_energy_exported_handler(driver, device, ib, response) - end - end -end - -local function initial_press_event_handler(driver, device, ib, response) - if get_field_for_endpoint(device, INITIAL_PRESS_ONLY, ib.endpoint_id) then - local sbe = device:get_field(SECOND_BUTTON_ENDPOINT) - if ib.endpoint_id == sbe then - local sse = device:get_field(SECOND_SWITCH_ENDPOINT) - local ssi = device:get_field(SECOND_SWITCH_ID) - local child_list = device:get_child_list() - for _, child in pairs(child_list) do - if child.id == ssi then - device:emit_event_for_endpoint(string.format("%d", sse), capabilities.button.button.pushed({state_change = true})) - break - end - end - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) - end - end -end - -local function active_power_handler(driver, device, ib, response) - if ib.data.value then - local watt_value = ib.data.value / CONVERSION_CONST_MILLIWATT_TO_WATT - device:emit_event(capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - end -end - -local function info_changed(driver, device, event, args) - if device.profile.id ~= args.old_st_store.profile.id or device:get_field(TEST_CONFIGURE) then - if device:get_field(DEFERRED_CONFIGURE) and device.network_type ~= device_lib.NETWORK_TYPE_CHILD then - -- profile has changed, and we deferred setting up our buttons, so do that now - - -- for unit testing - if device:get_field(TEST_CONFIGURE) then - initialize_switch(driver, device) - end - save_second_switch_id(device) - configure_buttons(device) - - -- Reset the values - if device:supports_capability(capabilities.powerMeter) then - device:emit_event(capabilities.powerMeter.power({ value = 0.0, unit = "W" })) - end - if device:supports_capability(capabilities.energyMeter) then - device:emit_event(capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) - end - - device:set_field(DEFERRED_CONFIGURE, nil) - end - end -end - -local function device_added(driver, device) - -- refresh child devices to get initial attribute state in case child device - -- was created after the initial subscription report - if device.network_type == device_lib.NETWORK_TYPE_CHILD then - handle_refresh(driver, device) - end -end - -local aqara_light_switch_h2_handler = { - NAME = "Aqara Light Switch H2 Handler", - lifecycle_handlers = { - init = device_init, - added = device_added, - removed = device_removed, - infoChanged = info_changed, - doConfigure = do_configure - }, - matter_handlers = { - attr = { - [clusters.OnOff.ID] = { - [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, - }, - [clusters.ElectricalPowerMeasurement.ID] = { - [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = active_power_handler, - }, - [clusters.ElectricalEnergyMeasurement.ID] = { - [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = energy_report_handler_factory(true), - }, - }, - event = { - [clusters.Switch.ID] = { - [clusters.Switch.events.InitialPress.ID] = initial_press_event_handler - }, - }, - fallback = matter_handler, - }, - subscribed_attributes = { - [capabilities.switch.ID] = { - clusters.OnOff.attributes.OnOff - }, - [capabilities.energyMeter.ID] = { - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, - }, - [capabilities.powerMeter.ID] = { - clusters.ElectricalPowerMeasurement.attributes.ActivePower - } - }, - subscribed_events = { - [capabilities.button.ID] = { - clusters.Switch.events.InitialPress - }, - }, - capability_handlers = { - [capabilities.refresh.ID] = { - [capabilities.refresh.commands.refresh.NAME] = handle_refresh, - }, - }, - supported_capabilities = { - capabilities.switch, - capabilities.powerMeter, - capabilities.energyMeter, - capabilities.powerConsumptionReport, - capabilities.button, - }, - can_handle = is_aqara_light_switch_h2 -} - -return aqara_light_switch_h2_handler - diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 1b1ff11fa5..6de14f7519 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -48,6 +48,7 @@ local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -- rather than COMPONENT_TO_ENDPOINT_MAP. local COMPONENT_TO_ENDPOINT_MAP_BUTTON = "__component_to_endpoint_map_button" local IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" +local IS_AQARA_SWITCH_DEVICE = "__is_aqara_switch_device" local COLOR_TEMP_BOUND_RECEIVED = "__colorTemp_bound_received" local COLOR_TEMP_MIN = "__color_temp_min" local COLOR_TEMP_MAX = "__color_temp_max" @@ -153,6 +154,8 @@ local device_type_attribute_map = { local child_device_profile_overrides = { { vendor_id = 0x1321, product_id = 0x000C, child_profile = "switch-binary" }, { vendor_id = 0x1321, product_id = 0x000D, child_profile = "switch-binary" }, + { vendor_id = 0x115F, product_id = 0x1008, child_profile = "light-power-energy-powerConsumption" }, -- 2 switch + { vendor_id = 0x115F, product_id = 0x1009, child_profile = "light-power-energy-powerConsumption" }, -- 4 switch } local detect_matter_thing @@ -270,6 +273,7 @@ local SUPPORTS_MULTI_PRESS = "__multi_button" -- for MSM devices (MomentarySwitc local INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease) local HUE_MANUFACTURER_ID = 0x100B +local AQARA_MANUFACTURER_ID = 0x115F --helper function to create list of multi press values local function create_multi_press_values_list(size, supportsHeld) @@ -405,7 +409,7 @@ local function find_default_endpoint(device) return device.MATTER_DEFAULT_ENDPOINT end -local function assign_child_profile(device, child_ep) +local function assign_child_profile(device, child_ep, ep_sequence) local profile -- check if device has an overridden child profile that differs from the profile @@ -413,7 +417,15 @@ local function assign_child_profile(device, child_ep) for _, fingerprint in ipairs(child_device_profile_overrides) do if device.manufacturer_info.vendor_id == fingerprint.vendor_id and device.manufacturer_info.product_id == fingerprint.product_id then - return fingerprint.child_profile + if device.manufacturer_info.vendor_id == AQARA_MANUFACTURER_ID then + if ep_sequence == 1 then + -- To add Electrical Sensor only to the first EDGE_CHILD + device:set_field(IS_AQARA_SWITCH_DEVICE, true) + return fingerprint.child_profile + end + else + return fingerprint.child_profile + end end end @@ -537,12 +549,14 @@ local function initialize_switch(driver, device) component_map_used = true end + local ep_sequence = 0 for _, ep in ipairs(switch_eps) do if device:supports_server_cluster(clusters.OnOff.ID, ep) then num_switch_server_eps = num_switch_server_eps + 1 if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint + ep_sequence = ep_sequence + 1 local name = string.format("%s %d", device.label, num_switch_server_eps) - local child_profile = assign_child_profile(device, ep) + local child_profile = assign_child_profile(device, ep, ep_sequence) driver:try_create_device( { type = "EDGE_CHILD", @@ -983,7 +997,11 @@ local function cumul_energy_exported_handler(driver, device, ib, response) if ib.data.elements.energy then local watt_hour_value = ib.data.elements.energy.value / CONVERSION_CONST_MILLIWATT_TO_WATT device:set_field(TOTAL_EXPORTED_ENERGY, watt_hour_value) - device:emit_event(capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) + if device:get_field(IS_AQARA_SWITCH_DEVICE) then + device:emit_event_for_endpoint(1, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) + else + device:emit_event(capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) + end end end @@ -1046,7 +1064,11 @@ end local function active_power_handler(driver, device, ib, response) if ib.data.value then local watt_value = ib.data.value / CONVERSION_CONST_MILLIWATT_TO_WATT - device:emit_event(capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + if device:get_field(IS_AQARA_SWITCH_DEVICE) then + device:emit_event_for_endpoint(1, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + else + device:emit_event(capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + end end end @@ -1132,8 +1154,18 @@ local function device_added(driver, device) device:emit_event(capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) end - -- call device init in case init is not called after added due to device caching - device_init(driver, device) + -- When device_init is performed in the aqara switch that has EDGE_CHILE, + -- a problem related to capability mapping occurs during configuration_buttons(), so this conditions are added. + if device.manufacturer_info.vendor_id ~= AQARA_MANUFACTURER_ID then + -- call device init in case init is not called after added due to device caching + device_init(driver, device) + else + if device.manufacturer_info.product_id ~= 0x1008 and + device.manufacturer_info.product_id ~= 0x1009 then + -- call device init in case init is not called after added due to device caching + device_init(driver, device) + end + end end local matter_driver_template = { @@ -1174,6 +1206,7 @@ local matter_driver_template = { [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = active_power_handler, }, [clusters.ElectricalEnergyMeasurement.ID] = { + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = energy_report_handler_factory(true), [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported.ID] = energy_report_handler_factory(true), [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported.ID] = energy_report_handler_factory(false), }, @@ -1234,6 +1267,7 @@ local matter_driver_template = { clusters.PowerSource.attributes.BatPercentRemaining, }, [capabilities.energyMeter.ID] = { + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported }, @@ -1293,8 +1327,7 @@ local matter_driver_template = { }, sub_drivers = { require("eve-energy"), - require("aqara-cube"), - require("aqara-light-switch-h2") + require("aqara-cube") } } diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua deleted file mode 100755 index c5324d16e5..0000000000 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua +++ /dev/null @@ -1,295 +0,0 @@ --- Copyright 2022 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 test = require "integration_test" -local capabilities = require "st.capabilities" -local t_utils = require "integration_test.utils" -local clusters = require "st.matter.clusters" - -local parent_ep = 1 -local child1_ep = 2 -local child2_ep = 5 -local child3_ep = 7 - --- used in unit testing, since device.profile.id and args.old_st_store.profile.id are always the same --- and this is to avoid the crash of the test case that occurs when try_update_metadata is performed in the device_init stage. -local TEST_CONFIGURE = "__test_configure" -local DEFERRED_CONFIGURE = "__DEFERRED_CONFIGURE" - -local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("light-button-electricalMeasurement.yml"), - manufacturer_info = {vendor_id = 0x115F, product_id = 0x1009, product_name = "Aqara Light Switch H2"}, - label = "Aqara Light Switch", - device_id = "00000000-1111-2222-3333-000000000001", - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = parent_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - {cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 2 }, - {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 5 } - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 1}, -- On/Off Light - {device_type_id = 0x0510, device_type_revision = 1}, -- Electrical Sensor - } - }, - { - endpoint_id = child1_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 1}, -- On/Off Light - } - }, - { - endpoint_id = 4, - clusters = { - {cluster_id = clusters.Switch.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH} - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = child2_ep, - clusters = { - {cluster_id = clusters.Switch.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH} - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 6, - clusters = { - {cluster_id = clusters.Switch.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH} - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = child3_ep, - clusters = { - {cluster_id = clusters.Switch.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH} - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - } - } -}) - --- add device for each mock device -local CLUSTER_SUBSCRIBE_LIST ={ - clusters.OnOff.attributes.OnOff, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete, - clusters.ElectricalPowerMeasurement.attributes.ActivePower, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, -} - -local child_profiles = { - [child1_ep] = t_utils.get_profile_definition("light-button.yml"), - [child2_ep] = t_utils.get_profile_definition("button.yml"), - [child3_ep] = t_utils.get_profile_definition("button.yml") -} - -local cumulative_report_val_19 = { - energy = 19000, - start_timestamp = 0, - end_timestamp = 0, - start_systime = 0, - end_systime = 0, -} - -local mock_children = {} -for i, endpoint in ipairs(mock_device.endpoints) do - if endpoint.endpoint_id == child1_ep or endpoint.endpoint_id == child2_ep or endpoint.endpoint_id == child3_ep then - local child_data = { - profile = child_profiles[endpoint.endpoint_id], - device_network_id = string.format("%s:%d", mock_device.id, endpoint.endpoint_id), - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) - } - mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) - end -end - -local function test_init() - local opts = { persist = true } - mock_device:set_field(TEST_CONFIGURE, true, opts) - - local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) - for i, cluster in ipairs(CLUSTER_SUBSCRIBE_LIST) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) - - for _, child in pairs(mock_children) do - test.mock_device.add_test_device(child) - end - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Aqara Light Switch 2", - profile = "light-button", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child1_ep) - }) - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Aqara Light Switch 3", - profile = "button", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child2_ep) - }) - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Aqara Light Switch 4", - profile = "button", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child3_ep) - }) -end - -test.set_test_init_function(test_init) - -test.register_message_test( - "First Switch device : switch capability should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, parent_ep) - }, - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, parent_ep, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) - } - } -) - -test.register_coroutine_test( - "First Switch device : switch/button capability should send the appropriate commands", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - mock_device:set_field(DEFERRED_CONFIGURE, true) - test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({value = 0.0})) - mock_device:expect_metadata_update({ profile = "light-button-electricalMeasurement" }) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("button", capabilities.button.supportedButtonValues({"pushed"}, {state_change = false})) - ) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 0.0, unit = "W" })) - ) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 0.0, unit = "Wh"})) - ) - - test.wait_for_events() - - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report(mock_device, 4, {new_position = 1}) - } - ) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("button", capabilities.button.button.pushed({state_change = true})) - ) - end -) - -test.register_coroutine_test( - "Check Energy/Power Management", function() - - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.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.matter:__queue_receive( - { - mock_device.id, - clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(mock_device, 1, 17000) - } - ) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.powerMeter.power({value = 17.0, unit="W"})) - ) - end -) - -test.run_registered_tests() - diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index e702362997..efdb319287 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -90,11 +90,13 @@ local mock_device_periodic = test.mock_device.build_test_matter_device({ local subscribed_attributes_periodic = { clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported, } local subscribed_attributes = { clusters.OnOff.attributes.OnOff, clusters.ElectricalPowerMeasurement.attributes.ActivePower, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, }