diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index bbf9ca24ee..c7ac1f4d4f 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -85,10 +85,10 @@ 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) + local default_endpoint_id = 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 ep.endpoint_id ~= default_endpoint_id 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 @@ -101,7 +101,7 @@ function SwitchLifecycleHandlers.device_init(driver, device) 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 + 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) 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 548ce67890..da8066f828 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,10 +27,6 @@ if version.api < 11 then clusters.PowerTopology = require "embedded_clusters.PowerTopology" end -if version.api < 16 then - clusters.Descriptor = require "embedded_clusters.Descriptor" -end - local aqara_parent_ep = 4 local aqara_child1_ep = 1 local aqara_child2_ep = 2 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_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua index 84ccc9e478..8b3bbd8ae2 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -77,6 +77,52 @@ local mock_device_no_hue_sat = test.mock_device.build_test_matter_device({ } }) +local mock_device_color_temp = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 1}, -- On/Off Light + {device_type_id = 0x010C, device_type_revision = 1} -- Color Temperature Light + } + } + } +}) + +local mock_device_extended_color = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("light-color-level.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 1}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 1}, -- Dimmable Light + {device_type_id = 0x010C, device_type_revision = 1}, -- Color Temperature Light + {device_type_id = 0x010D, device_type_revision = 1}, -- Extended Color Light + } + } + } +}) + local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -146,6 +192,67 @@ local function test_init_no_hue_sat() set_color_mode(mock_device_no_hue_sat, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) end + +local cluster_subscribe_list_color_temp = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds +} + +local function test_init_color_temp() + test.mock_device.add_test_device(mock_device_color_temp) + local subscribe_request = cluster_subscribe_list_color_temp[1]:subscribe(mock_device_color_temp) + for i, cluster in ipairs(cluster_subscribe_list_color_temp) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_color_temp)) + end + end + test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "added" }) + test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "init" }) + test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "doConfigure" }) + mock_device_color_temp:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_extended_color() + test.mock_device.add_test_device(mock_device_extended_color) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_extended_color) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_extended_color)) + end + end + test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "added" }) + test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "init" }) + test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "doConfigure" }) + mock_device_extended_color:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +test.register_message_test( + "Test that Color Temperature Light device does not switch profiles", + {}, + { test_init = test_init_color_temp } +) + +test.register_message_test( + "Test that Extended Color Light device does not switch profiles", + {}, + { test_init = test_init_extended_color } +) + test.register_message_test( "On command should send the appropriate commands", { 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..056c103eed 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 @@ -505,6 +505,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 +527,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 +568,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 +620,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..cff4c7b60b 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 @@ -189,6 +189,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 +261,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 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..323efea478 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 = "switch-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 b2d066ce61..23b5ca6873 100644 --- a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua @@ -31,97 +31,62 @@ local DeviceConfiguration = {} local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} -function SwitchDeviceConfiguration.assign_child_profile(device, child_ep) - local profile +function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_onoff_ep_id, is_child_device) + local ep_info = switch_utils.get_endpoint_info(device, server_onoff_ep_id) - 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 + -- per spec, the Switch device types support OnOff as CLIENT, though some vendors break spec and support it as SERVER. + local primary_dt_id = switch_utils.find_max_subset_device_type(ep_info, fields.DEVICE_TYPE_ID.LIGHT) + or switch_utils.find_max_subset_device_type(ep_info, fields.DEVICE_TYPE_ID.SWITCH) + or ep_info.device_types[1] and ep_info.device_types[1].device_type_id - -- vendor override checks - if child_ep == switch_utils.get_product_override_field(device, "ep_id") or profile == switch_utils.get_product_override_field(device, "initial_profile") then - profile = switch_utils.get_product_override_field(device, "target_profile") or profile - end + local generic_profile = fields.device_type_profile_map[primary_dt_id] - -- 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}) - end - end - end + if is_child_device and ( + server_onoff_ep_id == switch_utils.get_product_override_field(device, "ep_id") or + generic_profile == switch_utils.get_product_override_field(device, "initial_profile") + ) then + generic_profile = switch_utils.get_product_override_field(device, "target_profile") or generic_profile 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}) - end - - -- this is needed in initialize_buttons_and_switches - return num_switch_server_eps + -- if no supported device type is found, return switch-binary as a generic "OnOff EP" profile + return generic_profile or "switch-binary" 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_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id) + if #server_onoff_ep_ids == 1 and server_onoff_ep_ids[1] == default_endpoint_id then -- no children will be created + 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 ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint + local name = string.format("%s %d", device.label, device_num) + local child_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, ep_id, true) + 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_id), + vendor_provided_label = name + }) + 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) +function ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, num_button_eps) local profile_name = string.gsub(num_button_eps .. "-button", "1%-", "") -- remove the "1-" in a device with 1 button ep - if switch_utils.device_type_supports_button_switch_combination(device, main_endpoint) then + if switch_utils.device_type_supports_button_switch_combination(device, default_endpoint_id) then profile_name = "light-level-" .. profile_name end local motion_eps = device:get_endpoints(clusters.OccupancySensing.ID) @@ -136,13 +101,13 @@ function ButtonDeviceConfiguration.update_button_profile(device, main_endpoint, end end -function ButtonDeviceConfiguration.update_button_component_map(device, main_endpoint, button_eps) +function ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, button_eps) -- create component mapping on the main profile button endpoints table.sort(button_eps) local component_map = {} - component_map["main"] = main_endpoint + component_map["main"] = default_endpoint_id for component_num, ep in ipairs(button_eps) do - if ep ~= main_endpoint then + if ep ~= default_endpoint_id then local button_component = "button" if #button_eps > 1 then button_component = button_component .. component_num @@ -192,75 +157,61 @@ 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) +function DeviceConfiguration.match_profile(driver, device) + local default_endpoint_id = switch_utils.find_default_endpoint(device) + local updated_profile = nil - -- 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 + if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) > 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 + updated_profile = updated_profile .. "-level" + end 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 + local server_onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) -- get_endpoints defaults to return EPs supporting SERVER or BOTH + if #server_onoff_ep_ids > 0 then + SwitchDeviceConfiguration.create_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id) end - 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 #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, - {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then - profile_name = profile_name .. "-level" + if switch_utils.tbl_contains(server_onoff_ep_ids, default_endpoint_id) then + updated_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, default_endpoint_id) + local generic_profile = function(s) return string.find(updated_profile or "", s, 1, true) end + if generic_profile("plug-binary") or generic_profile("plug-level") then + if switch_utils.check_switch_category_vendor_overrides(device) then + updated_profile = string.gsub(updated_profile, "plug", "switch") + else + 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 + end + elseif generic_profile("light-color-level") and #device:get_endpoints(clusters.FanControl.ID) > 0 then + updated_profile = "light-color-level-fan" + elseif generic_profile("light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then + updated_profile = "light-level-motion" + elseif generic_profile("light-level-colorTemperature") or generic_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 - elseif #fan_eps > 0 then - profile_name = "light-color-level-fan" end - if profile_name then - device:try_update_metadata({ profile = profile_name }) + + -- 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, default_endpoint_id, #button_eps) + -- All button endpoints found will be added as additional components in the profile containing the default_endpoint_id. + ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, button_eps) + ButtonDeviceConfiguration.configure_buttons(device) + 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 07ad0fdbeb..dad46d4c92 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", } @@ -123,6 +127,31 @@ SwitchFields.vendor_overrides = { } } +SwitchFields.switch_category_vendor_overrides = { + [0x1432] = -- Elko + {0x1000}, + [0x130A] = -- Eve + {0x005D, 0x0043}, + [0x1339] = -- GE + {0x007D, 0x0074, 0x0075}, + [0x1372] = -- Innovation Matters + {0x0002}, + [0x1021] = -- Legrand + {0x0005}, + [0x109B] = -- Leviton + {0x1001, 0x1000, 0x100B, 0x100E, 0x100C, 0x100D, 0x1009, 0x1003, 0x1004, 0x1002}, + [0x142B] = -- LeTianPai + {0x1004, 0x1003, 0x1002}, + [0x1509] = -- SmartSetup + {0x0004, 0x0001}, + [0x1321] = -- SONOFF + {0x000B, 0x000C, 0x000D}, + [0x147F] = -- U-Tec + {0x0004}, + [0x139C] = -- Zemismart + {0xEEE2, 0xAB08, 0xAB31, 0xAB04, 0xAB01, 0xAB43, 0xAB02, 0xAB03, 0xAB05} +} + SwitchFields.CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" SwitchFields.TOTAL_IMPORTED_ENERGY = "__total_imported_energy" SwitchFields.LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" @@ -180,16 +209,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, @@ -198,7 +227,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, @@ -211,25 +240,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, @@ -242,14 +271,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 2558429654..2715f2e242 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 @@ -82,6 +83,14 @@ function utils.check_field_name_updates(device) end end +function utils.check_switch_category_vendor_overrides(device) + for _, product_id in ipairs(fields.switch_category_vendor_overrides[device.manufacturer_info.vendor_id] or {}) do + if device.manufacturer_info.product_id == product_id then + return true + end + end +end + --- device_type_supports_button_switch_combination helper function used to check --- whether the device type for an endpoint is currently supported by a profile for --- combination button/switch devices. @@ -89,10 +98,29 @@ function utils.device_type_supports_button_switch_combination(device, endpoint_i if utils.get_product_override_field(device, "ignore_combo_switch_button") then return false end - local dimmable_eps = utils.get_endpoints_by_device_type(device, fields.DIMMABLE_LIGHT_DEVICE_TYPE_ID) + local dimmable_eps = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.LIGHT.DIMMABLE) return utils.tbl_contains(dimmable_eps, endpoint_id) 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) @@ -128,9 +156,9 @@ function utils.find_default_endpoint(device) -- endpoint. If it is not a supported device type, return the first button endpoint as the -- default endpoint. if #switch_eps > 0 and #button_eps > 0 then - local main_endpoint = get_first_non_zero_endpoint(switch_eps) - if utils.device_type_supports_button_switch_combination(device, main_endpoint) then - return main_endpoint + local default_endpoint_id = get_first_non_zero_endpoint(switch_eps) + if utils.device_type_supports_button_switch_combination(device, default_endpoint_id) then + return default_endpoint_id else device.log.warn("The main switch endpoint does not contain a supported device type for a component configuration with buttons") return get_first_non_zero_endpoint(button_eps) @@ -163,6 +191,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)) @@ -193,7 +228,7 @@ function utils.create_multi_press_values_list(size, supportsHeld) end function utils.detect_bridge(device) - return #utils.get_endpoints_by_device_type(device, fields.AGGREGATOR_DEVICE_TYPE_ID) > 0 + return #utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.AGGREGATOR) > 0 end function utils.detect_matter_thing(device)