diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index bbf9ca24ee..cfa8ef778a 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -24,7 +24,6 @@ local fields = require "utils.switch_fields" local switch_utils = require "utils.switch_utils" local cfg = require "utils.device_configuration" local device_cfg = cfg.DeviceCfg -local switch_cfg = cfg.SwitchCfg local button_cfg = cfg.ButtonCfg local attribute_handlers = require "generic_handlers.attribute_handlers" @@ -65,16 +64,21 @@ end function SwitchLifecycleHandlers.info_changed(driver, device, event, args) if device.profile.id ~= args.old_st_store.profile.id then - device:subscribe() - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - if #button_eps > 0 and device.network_type == device_lib.NETWORK_TYPE_MATTER then - button_cfg.configure_buttons(device) + if device.network_type == device_lib.NETWORK_TYPE_MATTER then + device:subscribe() + button_cfg.configure_buttons(device, + device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + ) + elseif device.network_type == device_lib.NETWORK_TYPE_CHILD then + switch_utils.update_subscriptions(device:get_parent_device()) -- parent device required to scan through EPs and update subscriptions end end -end -function SwitchLifecycleHandlers.device_removed(driver, device) - device.log.info("device removed") + if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then + if device.matter_version.software ~= args.old_st_store.matter_version.software then + device_cfg.match_profile(driver, device) + end + end end function SwitchLifecycleHandlers.device_init(driver, device) @@ -85,33 +89,7 @@ function SwitchLifecycleHandlers.device_init(driver, device) if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then device:set_find_child(switch_utils.find_child) end - local main_endpoint = switch_utils.find_default_endpoint(device) - -- ensure subscription to all endpoint attributes- including those mapped to child devices - for idx, ep in ipairs(device.endpoints) do - if ep.endpoint_id ~= main_endpoint then - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - local child_profile = switch_cfg.assign_child_profile(device, ep) - if idx == 1 and string.find(child_profile, "energy") then - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) - end - end - local id = 0 - for _, dt in ipairs(ep.device_types) do - id = math.max(id, dt.device_type_id) - end - for _, attr in pairs(fields.device_type_attribute_map[id] or {}) do - if id == fields.GENERIC_SWITCH_ID and - attr ~= clusters.PowerSource.attributes.BatPercentRemaining and - attr ~= clusters.PowerSource.attributes.BatChargeLevel then - device:add_subscribed_event(attr) - else - device:add_subscribed_attribute(attr) - end - end - end - end - device:subscribe() + switch_utils.update_subscriptions(device) -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. @@ -127,6 +105,10 @@ function SwitchLifecycleHandlers.device_init(driver, device) end end +function SwitchLifecycleHandlers.device_removed(driver, device) + device.log.info("device removed") +end + local matter_driver_template = { lifecycle_handlers = { added = SwitchLifecycleHandlers.device_added, diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua index d2e99b2331..c82ad8477e 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua @@ -25,6 +25,7 @@ local button_attr = capabilities.button.button local aqara_mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("3-button-battery-temperature-humidity.yml"), manufacturer_info = {vendor_id = 0x115F, product_id = 0x2004, product_name = "Aqara Climate Sensor W100"}, + matter_version = {hardware = 1, software = 1}, label = "Climate Sensor W100", device_id = "00000000-1111-2222-3333-000000000001", endpoints = { 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 index 27fe47f11a..c88c17ae63 100644 --- 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 @@ -27,6 +27,7 @@ local aqara_child2_ep = 2 local aqara_mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("4-button.yml"), manufacturer_info = {vendor_id = 0x115F, product_id = 0x1009, product_name = "Aqara Light Switch H2"}, + matter_version = {hardware = 1, software = 1}, label = "Aqara Light Switch", device_id = "00000000-1111-2222-3333-000000000001", endpoints = { 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 d981af3110..06ec130878 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -56,7 +56,7 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} }, device_types = { - { device_type_id = 0x010A, device_type_revision = 1 } -- OnOff Plug + { device_type_id = 0x010B, device_type_revision = 1 }, -- Dimmable Plug In Unit } } }, @@ -88,10 +88,20 @@ local mock_device_periodic = test.mock_device.build_test_matter_device({ { device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor } }, + { + endpoint_id = 2, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + }, + device_types = { + { device_type_id = 0x010A, device_type_revision = 1 }, -- On Off Plug In Unit + } + } }, }) local subscribed_attributes_periodic = { + clusters.OnOff.attributes.OnOff, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, } diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua index 23023b3d02..79e5456610 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -12,6 +12,7 @@ local uint32 = require "st.matter.data_types.Uint32" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("button-battery.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, endpoints = { { endpoint_id = 0, diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua index d8b0116101..8a2c174789 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua @@ -8,11 +8,11 @@ local clusters = require "st.matter.generated.zap_clusters" local button_attr = capabilities.button.button -- Mock a 5-button device using endpoints non-consecutive endpoints -local mock_device = test.mock_device.build_test_matter_device( - { - profile = t_utils.get_profile_definition("5-button-battery.yml"), -- on a real device we would switch to this, rather than fingerprint to it - manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, - endpoints = { +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("5-button-battery.yml"), -- on a real device we would switch to this, rather than fingerprint to it + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { { endpoint_id = 0, clusters = {}, @@ -87,8 +87,7 @@ local mock_device = test.mock_device.build_test_matter_device( } }, }, -} -) +}) -- add device for each mock device local CLUSTER_SUBSCRIBE_LIST ={ diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua index af7511100e..4ecd6b7c75 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua @@ -25,6 +25,7 @@ local mock_device = test.mock_device.build_test_matter_device({ vendor_id = 0x0000, product_id = 0x0000, }, + matter_version = {hardware = 1, software = 1}, endpoints = { { endpoint_id = 0, @@ -105,6 +106,7 @@ local mock_device_mcd_unsupported_switch_device_type = test.mock_device.build_te vendor_id = 0x0000, product_id = 0x0000, }, + matter_version = {hardware = 1, software = 1}, endpoints = { { endpoint_id = 0, @@ -192,15 +194,11 @@ local function expect_configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) end --- All messages queued and expectations set are done before the driver is actually run local function test_init() - -- we dont want the integration test framework to generate init/doConfigure, we are doing that here - -- so we can set the proper expectations on those events. test.disable_startup_messages() test.mock_device.add_test_device(mock_device) -- make sure the cache is populated test.mock_device.add_test_device(mock_child) - -- added sets a bunch of fields on the device, and calls init local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end @@ -208,13 +206,10 @@ local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - -- init results in subscription interaction test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - --doConfigure sets the provisioning state to provisioned - mock_device:expect_metadata_update({ profile = "light-level-3-button" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", @@ -222,19 +217,12 @@ local function test_init() parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", mock_device_ep5) }) + mock_device:expect_metadata_update({ profile = "light-level-3-button" }) expect_configure_buttons() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - - -- simulate the profile change update taking affect and the device info changing - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "5-buttons-battery" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - expect_configure_buttons() + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.matter:__expect_send({mock_device.id, clusters.OnOff.attributes.OnOff:read(mock_device)}) test.socket.device_lifecycle:__queue_receive({ mock_child.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, clusters.OnOff.attributes.OnOff:read(mock_device)}) test.socket.device_lifecycle:__queue_receive({ mock_child.id, "init" }) mock_child:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.socket.device_lifecycle:__queue_receive({ mock_child.id, "doConfigure" }) @@ -452,18 +440,48 @@ test.register_coroutine_test( test.register_coroutine_test( "Test driver switched event", function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "driverSwitched" }) + mock_child:expect_metadata_update({ profile = "light-color-level" }) mock_device:expect_metadata_update({ profile = "light-level-3-button" }) expect_configure_buttons() - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-color-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", mock_device_ep5) - }) end ) +test.register_coroutine_test( + "Test info changed event with parent device profile update", + function() + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + local updated_device_profile = t_utils.get_profile_definition("light-level-3-button.yml") + updated_device_profile.id = "updated device profile id" + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_profile })) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + expect_configure_buttons() + end +) + +test.register_coroutine_test( + "Test info changed event with matter_version update", + function() + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } })) -- bump to 2 + mock_child:expect_metadata_update({ profile = "light-color-level" }) + mock_device:expect_metadata_update({ profile = "light-level-3-button" }) + expect_configure_buttons() + end +) + + -- run the tests test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index 0046244bcd..58b53efb51 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -24,6 +24,10 @@ local mock_device_onoff = test.mock_device.build_test_matter_device({ vendor_id = 0x0000, product_id = 0x0000, }, + matter_version = { + hardware = 1, + software = 1, + }, endpoints = { { endpoint_id = 0, @@ -505,6 +509,7 @@ local function test_init_mounted_on_off_control() test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "doConfigure" }) + mock_device_mounted_on_off_control:expect_metadata_update({ profile = "switch-binary" }) mock_device_mounted_on_off_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -526,6 +531,7 @@ local function test_init_mounted_dimmable_load_control() test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "doConfigure" }) + mock_device_mounted_dimmable_load_control:expect_metadata_update({ profile = "switch-level" }) mock_device_mounted_dimmable_load_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -566,6 +572,7 @@ local function test_init_parent_child_different_types() test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "doConfigure" }) + mock_device_parent_child_different_types:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_child_different_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) mock_device_parent_child_different_types:expect_device_create({ @@ -617,6 +624,7 @@ local function test_init_light_level_motion() test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "doConfigure" }) + mock_device_light_level_motion:expect_metadata_update({ profile = "light-level-motion" }) mock_device_light_level_motion:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index 21c9e1087d..4b8312e1ef 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -34,6 +34,10 @@ local mock_device = test.mock_device.build_test_matter_device({ vendor_id = 0x0000, product_id = 0x0000, }, + matter_version = { + hardware = 1, + software = 1, + }, endpoints = { { endpoint_id = 0, @@ -189,6 +193,7 @@ local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "light-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children) do @@ -260,6 +265,7 @@ local function test_init_parent_child_endpoints_non_sequential() test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" }) + mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "light-binary" }) mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children_non_sequential) do @@ -687,4 +693,14 @@ test.register_coroutine_test( { test_init = test_init_parent_child_endpoints_non_sequential } ) +test.register_coroutine_test( + "Test info changed event with matter_version update", + function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } })) -- bump to 2 + mock_children[child1_ep]:expect_metadata_update({ profile = "light-level" }) + mock_children[child2_ep]:expect_metadata_update({ profile = "light-color-level" }) + mock_device:expect_metadata_update({ profile = "light-binary" }) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index 03c898b7f5..da744cfd33 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -146,6 +146,7 @@ local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "plug-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children) do @@ -196,6 +197,7 @@ local function test_init_child_profile_override() test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "doConfigure" }) + mock_device_child_profile_override:expect_metadata_update({ profile = "plug-binary" }) mock_device_child_profile_override:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children_child_profile_override) do diff --git a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua index feb21ac193..a94538c616 100644 --- a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua @@ -31,103 +31,76 @@ local DeviceConfiguration = {} local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} -function SwitchDeviceConfiguration.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 = fields.device_type_profile_map[id] - break - end - end - - -- Check if device has an overridden child profile that differs from the profile that would match - -- the child's device type for the following two cases: - -- 1. To add Electrical Sensor only to the first EDGE_CHILD (light-power-energy-powerConsumption) - -- for the Aqara Light Switch H2. The profile of the second EDGE_CHILD for this device is - -- determined in the "for" loop above (e.g., light-binary) - -- 2. The selected profile for the child device matches the initial profile defined in - -- child_device_profile_overrides - for id, vendor in pairs(fields.child_device_profile_overrides_per_vendor_id) do - for _, fingerprint in ipairs(vendor) do - if device.manufacturer_info.product_id == fingerprint.product_id and - ((device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and child_ep == 1) or profile == fingerprint.initial_profile) then - return fingerprint.target_profile - end - end - end - - -- default to "switch-binary" if no profile is found - return profile or "switch-binary" -end - -function SwitchDeviceConfiguration.create_child_switch_devices(driver, device, main_endpoint) - local num_switch_server_eps = 0 - local parent_child_device = false - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - table.sort(switch_eps) - for idx, 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 = SwitchDeviceConfiguration.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 - } - ) - parent_child_device = true - if idx == 1 and string.find(child_profile, "energy") then - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) +function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, onoff_ep_id, is_child_device) + local ep = switch_utils.get_endpoint_info(device, onoff_ep_id) + local primary_dt_id = switch_utils.find_max_subset_device_type(ep, fields.DEVICE_TYPE_ID.LIGHT) + or (switch_utils.detect_matter_thing(device) and switch_utils.find_max_subset_device_type(ep, fields.DEVICE_TYPE_ID.SWITCH)) + or ep.device_types[1] and ep.device_types[1].device_type_id + local profile = fields.device_type_profile_map[primary_dt_id] + + if is_child_device then + -- Check if device has an overridden child profile that differs from the profile that would match + -- the child's device type for the following two cases: + -- 1. To add Electrical Sensor only to the first EDGE_CHILD (light-power-energy-powerConsumption) + -- for the Aqara Light Switch H2. The profile of the second EDGE_CHILD for this device is + -- determined in the "for" loop above (e.g., light-binary) + -- 2. The selected profile for the child device matches the initial profile defined in + -- child_device_profile_overrides + for _, vendor in pairs(fields.child_device_profile_overrides_per_vendor_id) do + for _, fingerprint in ipairs(vendor) do + if device.manufacturer_info.product_id == fingerprint.product_id and + ((device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and onoff_ep_id == 1) or profile == fingerprint.initial_profile) then + -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. + device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, onoff_ep_id, {persist = true}) + return fingerprint.target_profile end end end - end - -- If the device is a parent child device, set the find_child function on init. This is persisted because initialize_buttons_and_switches - -- is only run once, but find_child function should be set on each driver init. - if parent_child_device then - device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true}) + -- default to "switch-binary" if no profile is found + return profile or "switch-binary" end - -- this is needed in initialize_buttons_and_switches - return num_switch_server_eps + return profile end -function SwitchDeviceConfiguration.update_devices_with_onOff_server_clusters(device, main_endpoint) - local cluster_id = 0 - for _, ep in ipairs(device.endpoints) do - -- main_endpoint only supports server cluster by definition of get_endpoints() - if main_endpoint == ep.endpoint_id then - for _, dt in ipairs(ep.device_types) do - -- no device type that is not in the switch subset should be considered. - if (fields.ON_OFF_SWITCH_ID <= dt.device_type_id and dt.device_type_id <= fields.ON_OFF_COLOR_DIMMER_SWITCH_ID) then - cluster_id = math.max(cluster_id, dt.device_type_id) - end +function SwitchDeviceConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, main_endpoint_id) + if #server_onoff_ep_ids == 1 and server_onoff_ep_ids[1] == main_endpoint_id then -- no children will exist + return + end + + local device_num = 0 + table.sort(server_onoff_ep_ids) + for idx, ep_id in ipairs(server_onoff_ep_ids) do + device_num = device_num + 1 + if ep_id ~= main_endpoint_id then -- don't create a child device that maps to the main endpoint + local child_device_name = string.format("%s %d", device.label, device_num) + local child_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, ep_id, true) + local existing_child_device = device:get_field(fields.IS_PARENT_CHILD_DEVICE) and switch_utils.find_child(device, ep_id) + if not existing_child_device then + driver:try_create_device({ + type = "EDGE_CHILD", + label = child_device_name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%d", ep_id), + vendor_provided_label = child_device_name + }) + else + existing_child_device:try_update_metadata({ + profile = child_profile + }) + end + if idx == 1 and string.find(child_profile, "energy") then + -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. + device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep_id, {persist = true}) end - break end end - if fields.device_type_profile_map[cluster_id] then - device:try_update_metadata({profile = fields.device_type_profile_map[cluster_id]}) - end + -- Persist so that the find_child function is always set on each driver init. + device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true}) + device:set_find_child(switch_utils.find_child) end function ButtonDeviceConfiguration.update_button_profile(device, main_endpoint, num_button_eps) @@ -161,13 +134,12 @@ function ButtonDeviceConfiguration.update_button_component_map(device, main_endp end -function ButtonDeviceConfiguration.configure_buttons(device) - local ms_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) +function ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep_ids) local msr_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_RELEASE}) local msl_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS}) local msm_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS}) - for _, ep in ipairs(ms_eps) do + for _, ep in ipairs(momentary_switch_ep_ids or {}) do if device.profile.components[switch_utils.endpoint_to_component(device, ep)] then device.log.info_with({hub_logs=true}, string.format("Configuring Supported Values for generic switch endpoint %d", ep)) local supportedButtonValues_event @@ -199,75 +171,56 @@ end -- [[ PROFILE MATCHING AND CONFIGURATIONS ]] -- -function DeviceConfiguration.initialize_buttons_and_switches(driver, device, main_endpoint) - local profile_found = false - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - ButtonDeviceConfiguration.update_button_profile(device, main_endpoint, #button_eps) - -- All button endpoints found will be added as additional components in the profile containing the main_endpoint. - -- The resulting endpoint to component map is saved in the COMPONENT_TO_ENDPOINT_MAP field - ButtonDeviceConfiguration.update_button_component_map(device, main_endpoint, button_eps) - ButtonDeviceConfiguration.configure_buttons(device) - profile_found = true - end - - -- Without support for bindings, only clusters that are implemented as server are counted. This count is handled - -- while building switch child profiles - local num_switch_server_eps = SwitchDeviceConfiguration.create_child_switch_devices(driver, device, main_endpoint) - - -- We do not support the Light Switch device types because they require OnOff to be implemented as 'client', which requires us to support bindings. - -- However, this workaround profiles devices that claim to be Light Switches, but that break spec and implement OnOff as 'server'. - -- Note: since their device type isn't supported, these devices join as a matter-thing. - if num_switch_server_eps > 0 and switch_utils.detect_matter_thing(device) then - SwitchDeviceConfiguration.update_devices_with_onOff_server_clusters(device, main_endpoint) - profile_found = true - end - return profile_found -end - function DeviceConfiguration.match_profile(driver, device) - local main_endpoint = switch_utils.find_default_endpoint(device) - -- initialize the main device card with buttons if applicable, and create child devices as needed for multi-switch devices. - local profile_found = DeviceConfiguration.initialize_buttons_and_switches(driver, device, main_endpoint) - if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then - device:set_find_child(switch_utils.find_child) - end - if profile_found then - return - end + local main_endpoint_id = switch_utils.find_default_endpoint(device) + local updated_profile = nil - local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local level_eps = device:get_endpoints(clusters.LevelControl.ID) - local energy_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) - local power_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) local valve_eps = embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) - local profile_name = nil - local level_support = "" - if #level_eps > 0 then - level_support = "-level" - end - if #energy_eps > 0 and #power_eps > 0 then - profile_name = "plug" .. level_support .. "-power-energy-powerConsumption" - elseif #energy_eps > 0 then - profile_name = "plug" .. level_support .. "-energy-powerConsumption" - elseif #power_eps > 0 then - profile_name = "plug" .. level_support .. "-power" - elseif #valve_eps > 0 then - profile_name = "water-valve" + if #valve_eps > 0 then + updated_profile = "water-valve" if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then - profile_name = profile_name .. "-level" + updated_profile = updated_profile .. "-level" end - elseif #fan_eps > 0 then - profile_name = "light-color-level-fan" end - if profile_name then - device:try_update_metadata({ profile = profile_name }) + + local server_onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID, { cluster_type = "SERVER" }) + if #server_onoff_ep_ids > 0 then + SwitchDeviceConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, main_endpoint_id) + updated_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, main_endpoint_id) + local find_substr = function(s, p) return string.find(s or "", p, 1, true) end + + if find_substr(updated_profile, "plug-binary") or find_substr(updated_profile, "plug-level") then + local electrical_tags = "" + if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) > 0 then electrical_tags = electrical_tags .. "-power" end + if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) > 0 then electrical_tags = electrical_tags .. "-energy-powerConsumption" end + if electrical_tags ~= "" then updated_profile = string.gsub(updated_profile, "-binary", "") .. electrical_tags end + elseif find_substr(updated_profile, "light-color-level") and #device:get_endpoints(clusters.FanControl.ID) > 0 then + updated_profile = "light-color-level-fan" + elseif find_substr(updated_profile, "light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then + updated_profile = "light-level-motion" + elseif find_substr(updated_profile, "light-level-colorTemperature") or find_substr(updated_profile, "light-color-level") then + -- ignore attempts to dynamically profile light-level-colorTemperature and light-color-level devices for now, since + -- these may lose fingerprinted Kelvin ranges when dynamically profiled. + return + end end + + -- initialize the main device card with buttons if applicable + local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + ButtonDeviceConfiguration.update_button_profile(device, main_endpoint_id, #button_eps) + -- All button endpoints found will be added as additional components in the profile containing the main_endpoint. + ButtonDeviceConfiguration.update_button_component_map(device, main_endpoint_id, button_eps) + ButtonDeviceConfiguration.configure_buttons(device, button_eps) + return + end + + device:try_update_metadata({ profile = updated_profile }) end return { DeviceCfg = DeviceConfiguration, SwitchCfg = SwitchDeviceConfiguration, ButtonCfg = ButtonDeviceConfiguration -} +} \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua index 2244eab661..fe3a2d9848 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua @@ -45,35 +45,39 @@ SwitchFields.SWITCH_LEVEL_LIGHTING_MIN = 1 SwitchFields.CURRENT_HUESAT_ATTR_MIN = 0 SwitchFields.CURRENT_HUESAT_ATTR_MAX = 254 - --- DEVICE TYPES -SwitchFields.AGGREGATOR_DEVICE_TYPE_ID = 0x000E -SwitchFields.ON_OFF_LIGHT_DEVICE_TYPE_ID = 0x0100 -SwitchFields.DIMMABLE_LIGHT_DEVICE_TYPE_ID = 0x0101 -SwitchFields.COLOR_TEMP_LIGHT_DEVICE_TYPE_ID = 0x010C -SwitchFields.EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID = 0x010D -SwitchFields.ON_OFF_PLUG_DEVICE_TYPE_ID = 0x010A -SwitchFields.DIMMABLE_PLUG_DEVICE_TYPE_ID = 0x010B -SwitchFields.ON_OFF_SWITCH_ID = 0x0103 -SwitchFields.ON_OFF_DIMMER_SWITCH_ID = 0x0104 -SwitchFields.ON_OFF_COLOR_DIMMER_SWITCH_ID = 0x0105 -SwitchFields.MOUNTED_ON_OFF_CONTROL_ID = 0x010F -SwitchFields.MOUNTED_DIMMABLE_LOAD_CONTROL_ID = 0x0110 -SwitchFields.GENERIC_SWITCH_ID = 0x000F -SwitchFields.ELECTRICAL_SENSOR_ID = 0x0510 +SwitchFields.DEVICE_TYPE_ID = { + AGGREGATOR = 0x000E, + DIMMABLE_PLUG_IN_UNIT = 0x010B, + ELECTRICAL_SENSOR = 0x0510, + GENERIC_SWITCH = 0x000F, + MOUNTED_ON_OFF_CONTROL = 0x010F, + MOUNTED_DIMMABLE_LOAD_CONTROL = 0x0110, + ON_OFF_PLUG_IN_UNIT = 0x010A, + LIGHT = { + ON_OFF = 0x0100, + DIMMABLE = 0x0101, + COLOR_TEMPERATURE = 0x010C, + EXTENDED_COLOR = 0x010D, + }, + SWITCH = { + ON_OFF_LIGHT = 0x0103, + DIMMER = 0x0104, + COLOR_DIMMER = 0x0105, + }, +} SwitchFields.device_type_profile_map = { - [SwitchFields.ON_OFF_LIGHT_DEVICE_TYPE_ID] = "light-binary", - [SwitchFields.DIMMABLE_LIGHT_DEVICE_TYPE_ID] = "light-level", - [SwitchFields.COLOR_TEMP_LIGHT_DEVICE_TYPE_ID] = "light-level-colorTemperature", - [SwitchFields.EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID] = "light-color-level", - [SwitchFields.ON_OFF_PLUG_DEVICE_TYPE_ID] = "plug-binary", - [SwitchFields.DIMMABLE_PLUG_DEVICE_TYPE_ID] = "plug-level", - [SwitchFields.ON_OFF_SWITCH_ID] = "switch-binary", - [SwitchFields.ON_OFF_DIMMER_SWITCH_ID] = "switch-level", - [SwitchFields.ON_OFF_COLOR_DIMMER_SWITCH_ID] = "switch-color-level", - [SwitchFields.MOUNTED_ON_OFF_CONTROL_ID] = "switch-binary", - [SwitchFields.MOUNTED_DIMMABLE_LOAD_CONTROL_ID] = "switch-level", + [SwitchFields.DEVICE_TYPE_ID.LIGHT.ON_OFF] = "light-binary", + [SwitchFields.DEVICE_TYPE_ID.LIGHT.DIMMABLE] = "light-level", + [SwitchFields.DEVICE_TYPE_ID.LIGHT.COLOR_TEMPERATURE] = "light-level-colorTemperature", + [SwitchFields.DEVICE_TYPE_ID.LIGHT.EXTENDED_COLOR] = "light-color-level", + [SwitchFields.DEVICE_TYPE_ID.SWITCH.ON_OFF_LIGHT] = "switch-binary", + [SwitchFields.DEVICE_TYPE_ID.SWITCH.DIMMER] = "switch-level", + [SwitchFields.DEVICE_TYPE_ID.SWITCH.COLOR_DIMMER] = "switch-color-level", + [SwitchFields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT] = "plug-binary", + [SwitchFields.DEVICE_TYPE_ID.DIMMABLE_PLUG_IN_UNIT] = "plug-level", + [SwitchFields.DEVICE_TYPE_ID.MOUNTED_ON_OFF_CONTROL] = "switch-binary", + [SwitchFields.DEVICE_TYPE_ID.MOUNTED_DIMMABLE_LOAD_CONTROL] = "switch-level", } @@ -182,16 +186,16 @@ SwitchFields.supported_capabilities = { } SwitchFields.device_type_attribute_map = { - [SwitchFields.ON_OFF_LIGHT_DEVICE_TYPE_ID] = { + [SwitchFields.DEVICE_TYPE_ID.LIGHT.ON_OFF] = { clusters.OnOff.attributes.OnOff }, - [SwitchFields.DIMMABLE_LIGHT_DEVICE_TYPE_ID] = { + [SwitchFields.DEVICE_TYPE_ID.LIGHT.DIMMABLE] = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, clusters.LevelControl.attributes.MinLevel }, - [SwitchFields.COLOR_TEMP_LIGHT_DEVICE_TYPE_ID] = { + [SwitchFields.DEVICE_TYPE_ID.LIGHT.COLOR_TEMPERATURE] = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, @@ -200,7 +204,7 @@ SwitchFields.device_type_attribute_map = { clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, clusters.ColorControl.attributes.ColorTempPhysicalMinMireds }, - [SwitchFields.EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID] = { + [SwitchFields.DEVICE_TYPE_ID.LIGHT.EXTENDED_COLOR] = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, @@ -213,25 +217,25 @@ SwitchFields.device_type_attribute_map = { clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY }, - [SwitchFields.ON_OFF_PLUG_DEVICE_TYPE_ID] = { + [SwitchFields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT] = { clusters.OnOff.attributes.OnOff }, - [SwitchFields.DIMMABLE_PLUG_DEVICE_TYPE_ID] = { + [SwitchFields.DEVICE_TYPE_ID.DIMMABLE_PLUG_IN_UNIT] = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, clusters.LevelControl.attributes.MinLevel }, - [SwitchFields.ON_OFF_SWITCH_ID] = { + [SwitchFields.DEVICE_TYPE_ID.SWITCH.ON_OFF_LIGHT] = { clusters.OnOff.attributes.OnOff }, - [SwitchFields.ON_OFF_DIMMER_SWITCH_ID] = { + [SwitchFields.DEVICE_TYPE_ID.SWITCH.DIMMER] = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, clusters.LevelControl.attributes.MinLevel }, - [SwitchFields.ON_OFF_COLOR_DIMMER_SWITCH_ID] = { + [SwitchFields.DEVICE_TYPE_ID.SWITCH.COLOR_DIMMER] = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, @@ -244,14 +248,14 @@ SwitchFields.device_type_attribute_map = { clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY }, - [SwitchFields.GENERIC_SWITCH_ID] = { + [SwitchFields.DEVICE_TYPE_ID.GENERIC_SWITCH] = { clusters.PowerSource.attributes.BatPercentRemaining, clusters.Switch.events.InitialPress, clusters.Switch.events.LongPress, clusters.Switch.events.ShortRelease, clusters.Switch.events.MultiPressComplete }, - [SwitchFields.ELECTRICAL_SENSOR_ID] = { + [SwitchFields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR] = { clusters.ElectricalPowerMeasurement.attributes.ActivePower, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua index 86368e9208..83e5f45924 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua @@ -21,7 +21,8 @@ local log = require "log" local utils = {} function utils.tbl_contains(array, value) - for _, element in ipairs(array) do + if value == nil then return false end + for _, element in pairs(array or {}) do if element == value then return true end @@ -81,7 +82,7 @@ function utils.device_type_supports_button_switch_combination(device, endpoint_i for _, ep in ipairs(device.endpoints) do if ep.endpoint_id == endpoint_id then for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == fields.DIMMABLE_LIGHT_DEVICE_TYPE_ID then + if dt.device_type_id == fields.DEVICE_TYPE_ID.LIGHT.DIMMABLE then for _, fingerprint in ipairs(fields.child_device_profile_overrides_per_vendor_id[0x115F]) do if device.manufacturer_info.product_id == fingerprint.product_id then return false -- For Aqara Dimmer Switch with Button. @@ -95,6 +96,25 @@ function utils.device_type_supports_button_switch_combination(device, endpoint_i return false end +-- Some devices report multiple device types which are a subset of +-- a superset device type (Ex. Dimmable Light is a superset of On/Off Light). +-- We should map to the largest superset device type supported. +-- This can be done by matching to the device type with the highest ID +function utils.find_max_subset_device_type(ep, device_type_set) + if ep.endpoint_id == 0 then return end -- EP-scoped device types not permitted on Root Node + local primary_dt_id = ep.device_types[1] and ep.device_types[1].device_type_id + if utils.tbl_contains(device_type_set, primary_dt_id) then + for _, dt in ipairs(ep.device_types) do + -- only device types in the subset should be considered. + if utils.tbl_contains(device_type_set, dt.device_type_id) then + primary_dt_id = math.max(primary_dt_id, dt.device_type_id) + end + end + return primary_dt_id + end + return nil +end + --- find_default_endpoint is a helper function to handle situations where --- device does not have endpoint ids in sequential order from 1 function utils.find_default_endpoint(device) @@ -166,6 +186,13 @@ function utils.find_child(parent, ep_id) return parent:get_child_by_parent_assigned_key(string.format("%d", ep_id)) end +function utils.get_endpoint_info(device, endpoint_id) + for _, ep in ipairs(device.endpoints) do + if ep.endpoint_id == endpoint_id then return ep end + end + return {} +end + -- Fallback handler for responses that dont have their own handler function utils.matter_handler(driver, device, response_block) device.log.info(string.format("Fallback handler for %s", response_block)) @@ -185,7 +212,7 @@ end function utils.detect_bridge(device) for _, ep in ipairs(device.endpoints) do for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == fields.AGGREGATOR_DEVICE_TYPE_ID then + if dt.device_type_id == fields.DEVICE_TYPE_ID.AGGREGATOR then return true end end @@ -194,12 +221,11 @@ function utils.detect_bridge(device) end function utils.detect_matter_thing(device) - for _, capability in ipairs(fields.supported_capabilities) do - if device:supports_capability(capability) then - return false - end + -- every profile except for matter-thing supports at least 2 capabilities (refresh, firmwareUpdate) + for i, _ in ipairs(device.profile.components.main.capabilities) do + if i > 1 then return false end end - return device:supports_capability(capabilities.refresh) + return true end function utils.report_power_consumption_to_st_energy(device, latest_total_imported_energy_wh) @@ -241,4 +267,27 @@ function utils.report_power_consumption_to_st_energy(device, latest_total_import end end +function utils.update_subscriptions(device) + local main_endpoint = utils.find_default_endpoint(device) + -- ensure subscription to all endpoint attributes- including those mapped to child devices + for idx, ep in ipairs(device.endpoints) do + if ep.endpoint_id ~= main_endpoint then + local id = 0 + for _, dt in ipairs(ep.device_types) do + id = math.max(id, dt.device_type_id) + end + for _, attr in pairs(fields.device_type_attribute_map[id] or {}) do + if id == fields.DEVICE_TYPE_ID.GENERIC_SWITCH and + attr ~= clusters.PowerSource.attributes.BatPercentRemaining and + attr ~= clusters.PowerSource.attributes.BatChargeLevel then + device:add_subscribed_event(attr) + else + device:add_subscribed_attribute(attr) + end + end + end + end + device:subscribe() +end + return utils