diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml index f6c45ed1f7..e81d7dc700 100755 --- a/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml @@ -1,24 +1,24 @@ name: light-level-power-energy-powerConsumption components: - - id: main - capabilities: - - id: switch - version: 1 - - id: switchLevel - version: 1 - config: - values: - - key: "level.value" - range: [1, 100] - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - - id: powerConsumptionReport - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: Light +- id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml index 1e64f63594..791b7a0692 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml @@ -1,18 +1,18 @@ name: light-power-energy-powerConsumption components: - - id: main - capabilities: - - id: switch - version: 1 - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - - id: powerConsumptionReport - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: Light +- id: main + capabilities: + - id: switch + version: 1 + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index c4e9d6dceb..2d5f0d80a7 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -50,7 +50,6 @@ local CURRENT_HUESAT_ATTR_MAX = 254 -- table for devices that joined prior to this transition, and is also used for -- button devices that require component mapping. local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -local ENERGY_MANAGEMENT_ENDPOINT = "__energy_management_endpoint" local IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" local COLOR_TEMP_BOUND_RECEIVED_KELVIN = "__colorTemp_bound_received_kelvin" local COLOR_TEMP_BOUND_RECEIVED_MIRED = "__colorTemp_bound_received_mired" @@ -63,7 +62,8 @@ local COLOR_MODE = "__color_mode" local updated_fields = { { current_field_name = "__component_to_endpoint_map_button", updated_field_name = COMPONENT_TO_ENDPOINT_MAP }, - { current_field_name = "__switch_intialized", updated_field_name = nil } + { current_field_name = "__switch_intialized", updated_field_name = nil }, + { current_field_name = "__energy_management_endpoint", updated_field_name = nil } } local HUE_SAT_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION @@ -174,20 +174,21 @@ local device_type_attribute_map = { } } -local child_device_profile_overrides_per_vendor_id = { +local device_overrides_per_vendor = { [0x1321] = { { product_id = 0x000C, target_profile = "switch-binary", initial_profile = "plug-binary" }, { product_id = 0x000D, target_profile = "switch-binary", initial_profile = "plug-binary" }, }, [0x115F] = { - { product_id = 0x1003, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1004, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x1005, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) - { product_id = 0x1006, target_profile = "light-level-power-energy-powerConsumption" }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) - { product_id = 0x1008, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1009, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x100A, target_profile = "light-level-power-energy-powerConsumption" }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) + { product_id = 0x1003, combo_switch_button = false }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) + { product_id = 0x1004, combo_switch_button = false }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) + { product_id = 0x1005, combo_switch_button = false }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) + { product_id = 0x1006, combo_switch_button = false }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) + { product_id = 0x1008, combo_switch_button = false }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) + { product_id = 0x1009, combo_switch_button = false }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) + { product_id = 0x100A, combo_switch_button = false }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) } + } local detect_matter_thing @@ -203,6 +204,12 @@ local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in s local SUBSCRIPTION_REPORT_OCCURRED = "__subscription_report_occurred" local CONVERSION_CONST_MILLIWATT_TO_WATT = 1000 -- A milliwatt is 1/1000th of a watt +local ELECTRICAL_SENSOR_EPS = "__ELECTRICAL_SENSOR_EPS" + +local profiling_data = { + ELECTRICAL_TOPOLOGY = "__ELECTRICAL_TOPOLOGY", +} + -- Return an ISO-8061 timestamp in UTC local function iso8061Timestamp(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) @@ -230,28 +237,30 @@ local function send_import_poll_report(device, latest_total_imported_energy_wh) energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) end - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - if not device:get_field(ENERGY_MANAGEMENT_ENDPOINT) then - device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ - start = iso8061Timestamp(last_time), - ["end"] = iso8061Timestamp(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) - else - device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT),capabilities.powerConsumptionReport.powerConsumption({ - start = iso8061Timestamp(last_time), - ["end"] = iso8061Timestamp(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) + -- Report the energy consumed during the time interval on the first device that supports it. The unit of these values should be 'Wh' + if device:get_parent_device() and device:get_parent_device():supports_capability(capabilities.powerConsumptionReport) then + device = device:get_parent_device() + elseif device:get_parent_device() ~= nil then + device = device:get_parent_device():get_child_list()[1] end + local component = device.profile.components["main"] + device:emit_component_event(component, capabilities.powerConsumptionReport.powerConsumption({ + start = iso8061Timestamp(last_time), + ["end"] = iso8061Timestamp(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) + end local function create_poll_report_schedule(device) local import_timer = device.thread:call_on_schedule( device:get_field(IMPORT_REPORT_TIMEOUT), function() - send_import_poll_report(device, device:get_field(TOTAL_IMPORTED_ENERGY)) + local total_imported_energy = 0 + for _, energy_report in pairs(device:get_field(TOTAL_IMPORTED_ENERGY)) do + total_imported_energy = total_imported_energy + energy_report + end + send_import_poll_report(device, total_imported_energy) end, "polling_import_report_schedule_timer" ) device:set_field(RECURRING_IMPORT_REPORT_POLL_TIMER, import_timer) @@ -275,10 +284,8 @@ local function set_poll_report_timer_and_schedule(device, is_cumulative_report) local second_timestamp = os.time() local report_interval_secs = second_timestamp - first_timestamp device:set_field(IMPORT_REPORT_TIMEOUT, math.max(report_interval_secs, MINIMUM_ST_ENERGY_REPORT_INTERVAL)) - -- the poll schedule is only needed for devices that support powerConsumption - -- and enable powerConsumption when energy management is defined in root endpoint(0). - if device:supports_capability(capabilities.powerConsumptionReport) or - device:get_field(ENERGY_MANAGEMENT_ENDPOINT) then + -- the poll schedule is only needed for devices that support powerConsumptionReport + if device:supports_capability(capabilities.powerConsumptionReport) then create_poll_report_schedule(device) end device:set_field(IMPORT_POLL_TIMER_SETTING_ATTEMPTED, true) @@ -321,6 +328,19 @@ local function create_multi_press_values_list(size, supportsHeld) return list end +-- get a list of endpoints for a specified device type. +local function get_endpoints_by_dt(device, device_type_id) + local dt_eps = {} + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == device_type_id then + table.insert(dt_eps, ep.endpoint_id) + end + end + end + return dt_eps +end + local function tbl_contains(array, value) for _, element in ipairs(array) do if element == value then @@ -385,21 +405,13 @@ end --- whether the device type for an endpoint is currently supported by a profile for --- combination button/switch devices. local function device_type_supports_button_switch_combination(device, endpoint_id) - 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 == DIMMABLE_LIGHT_DEVICE_TYPE_ID then - for _, fingerprint in ipairs(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. - end - end - return true - end - end + for _, fingerprint in ipairs(device_overrides_per_vendor[AQARA_MANUFACTURER_ID] or {}) do + if fingerprint.product_id == device.manufacturer_info.product_id and fingerprint.combo_switch_button == false then + return false -- For Aqara Dimmer Switch with Button. end end - return false + local dimmable_eps = get_endpoints_by_dt(device, DIMMABLE_LIGHT_DEVICE_TYPE_ID) + return tbl_contains(dimmable_eps, endpoint_id) end local function get_first_non_zero_endpoint(endpoints) @@ -480,43 +492,42 @@ local function check_field_name_updates(device) end end -local function assign_child_profile(device, child_ep) +local function assign_switch_profile(device, switch_ep, is_child_device, electrical_tags) local profile - for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == child_ep then + if ep.endpoint_id == switch_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 + -- Note: Electrical Sensor does not follow the above logic, so it's ignored local id = 0 for _, dt in ipairs(ep.device_types) do - id = math.max(id, dt.device_type_id) + if dt.device_type_id ~= ELECTRICAL_SENSOR_ID then + id = math.max(id, dt.device_type_id) + end end profile = 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(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 == AQARA_MANUFACTURER_ID and child_ep == 1) or profile == fingerprint.initial_profile) then - return fingerprint.target_profile - end - end + if electrical_tags ~= nil and (profile == "plug-binary" or profile == "plug-level" or profile == "light-binary") then + profile = string.gsub(profile, "-binary", "") .. electrical_tags end - -- default to "switch-binary" if no profile is found - return profile or "switch-binary" + if is_child_device then + -- Check if child device has an overridden child profile that differs from the child's generic device type profile + for _, fingerprint in ipairs(device_overrides_per_vendor[device.manufacturer_info.vendor_id] or {}) do + if device.manufacturer_info.product_id == fingerprint.product_id and profile == fingerprint.initial_profile then + return fingerprint.target_profile + end + end + -- default to "switch-binary" if no child profile is found + return profile or "switch-binary" + end + return profile end local function configure_buttons(device) @@ -598,7 +609,8 @@ local function build_child_switch_profiles(driver, device, main_endpoint) 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) + local electrical_tags = device:get_field(profiling_data.ELECTRICAL_TOPOLOGY).tags_on_ep[ep] + local child_profile = assign_switch_profile(device, ep, true, electrical_tags) driver:try_create_device( { type = "EDGE_CHILD", @@ -610,10 +622,6 @@ local function build_child_switch_profiles(driver, device, main_endpoint) } ) parent_child_device = true - if _ == 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(ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) - end end end end @@ -648,8 +656,7 @@ local function handle_light_switch_with_onOff_server_clusters(device, main_endpo end end -local function initialize_buttons_and_switches(driver, device, main_endpoint) - local profile_found = false +local function initialize_buttons(driver, device, main_endpoint) local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then build_button_profile(device, main_endpoint, #button_eps) @@ -657,32 +664,59 @@ local function initialize_buttons_and_switches(driver, device, main_endpoint) -- The resulting endpoint to component map is saved in the COMPONENT_TO_ENDPOINT_MAP field build_button_component_map(device, main_endpoint, button_eps) configure_buttons(device) - profile_found = true + return true end +end +local function collect_and_store_electrical_sensor_info(driver, device) + local el_dt_eps = get_endpoints_by_dt(device, ELECTRICAL_SENSOR_ID) + local electrical_sensor_eps = {} + local avail_eps_req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, ep in ipairs(device.endpoints) do + if tbl_contains(el_dt_eps, ep.endpoint_id) then + local electrical_ep_info = {} + electrical_ep_info.endpoint_id = ep.endpoint_id + for _, cluster in ipairs(ep.clusters) do + if cluster.cluster_id == clusters.ElectricalEnergyMeasurement.ID then + electrical_ep_info.energy = true + elseif cluster.cluster_id == clusters.ElectricalPowerMeasurement.ID then + electrical_ep_info.power = true + elseif cluster.cluster_id == clusters.PowerTopology.ID then + electrical_ep_info.topology = cluster.feature_map + if cluster.feature_map == clusters.PowerTopology.types.Feature.SET_TOPOLOGY then + avail_eps_req:merge(clusters.PowerTopology.attributes.AvailableEndpoints:read(device, ep.endpoint_id)) + electrical_ep_info.availableEndpoints = false + end + end + end + table.insert(electrical_sensor_eps, electrical_ep_info) + end + end + if #avail_eps_req.info_blocks ~= 0 then + device:send(avail_eps_req) + end + device:set_field(ELECTRICAL_SENSOR_EPS, electrical_sensor_eps) +end + +local function initialize_switches(driver, device, main_endpoint) -- 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 = build_child_switch_profiles(driver, device, main_endpoint) + if device:get_field(IS_PARENT_CHILD_DEVICE) then + device:set_find_child(find_child) + end -- 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 detect_matter_thing(device) then handle_light_switch_with_onOff_server_clusters(device, main_endpoint) - profile_found = true + return true end - return profile_found end local function detect_bridge(device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == AGGREGATOR_DEVICE_TYPE_ID then - return true - end - end - end - return false + return #get_endpoints_by_dt(device, AGGREGATOR_DEVICE_TYPE_ID) > 0 end local function device_init(driver, device) @@ -699,6 +733,11 @@ local function device_init(driver, device) if ep.endpoint_id ~= main_endpoint then local id = 0 for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == ELECTRICAL_SENSOR_ID then + for _, attr in pairs(device_type_attribute_map[ELECTRICAL_SENSOR_ID]) do + device:add_subscribed_attribute(attr) + end + end id = math.max(id, dt.device_type_id) end for _, attr in pairs(device_type_attribute_map[id] or {}) do @@ -716,34 +755,31 @@ local function device_init(driver, device) end end +local function profiling_data_still_required(device) + for _, field in pairs(profiling_data) do + if device:get_field(field) == nil then + return true -- data still required if a field is nil + end + end + return false +end + local function match_profile(driver, device) + if profiling_data_still_required(device) then return end + local main_endpoint = 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 = initialize_buttons_and_switches(driver, device, main_endpoint) - if device:get_field(IS_PARENT_CHILD_DEVICE) then - device:set_find_child(find_child) - end - if profile_found then + local button_profile_found = initialize_buttons(driver, device, main_endpoint) + local switch_profile_found = initialize_switches(driver, device, main_endpoint) + if button_profile_found or switch_profile_found then return 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 + if #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 @@ -754,6 +790,21 @@ local function match_profile(driver, device) end if profile_name then device:try_update_metadata({ profile = profile_name }) + return + end + + -- after doing all previous profiling steps, attempt to re-profile main/parent switch/plug device + local electrical_tags = device:get_field(profiling_data.ELECTRICAL_TOPOLOGY).tags_on_ep[main_endpoint] + profile_name = assign_switch_profile(device, main_endpoint, false, electrical_tags) + -- 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. + if profile_name and profile_name ~= "light-level-colorTemperature" and profile_name ~= "light-color-level" then + device:try_update_metadata({profile = profile_name}) + end + + -- clear all profiling data fields after profiling is complete. + for _, field in pairs(profiling_data) do + device:set_field(field, nil) end end @@ -1125,26 +1176,56 @@ local function occupancy_attr_handler(driver, device, ib, response) device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) end +local function available_endpoints_handler(driver, device, ib, response) + local electrical_sensor_eps = device:get_field(ELECTRICAL_SENSOR_EPS) + local avail_eps_req_complete = true + local electrical_tags_per_ep = {} + table.sort(ib.data.elements) + for _, info in ipairs(electrical_sensor_eps or {}) do + if info.endpoint_id == ib.endpoint_id then + info.availableEndpoints = ib.data.elements + elseif info.availableEndpoints == false then -- an endpoint is found that hasn't been updated through this handler. + avail_eps_req_complete = false + end + if avail_eps_req_complete then + local electrical_tags = "" + if info.power then electrical_tags = electrical_tags .. "-power" end + if info.energy then electrical_tags = electrical_tags .. "-energy-powerConsumption" end + electrical_tags_per_ep[info.availableEndpoints[1].value] = electrical_tags -- set tags on first available endpoint + end + end + if avail_eps_req_complete == false then + device:set_field(ELECTRICAL_SENSOR_EPS, electrical_sensor_eps) + else + local topology_data = {} + topology_data.topology = clusters.PowerTopology.types.Feature.SET_TOPOLOGY + topology_data.tags_on_ep = electrical_tags_per_ep + device:set_field(profiling_data.ELECTRICAL_TOPOLOGY, topology_data) + device:set_field(ELECTRICAL_SENSOR_EPS, nil) + match_profile(driver, device) + end +end + local function cumul_energy_imported_handler(driver, device, ib, response) if ib.data.elements.energy then + local component = device.profile.components["main"] local watt_hour_value = ib.data.elements.energy.value / CONVERSION_CONST_MILLIWATT_TO_WATT - device:set_field(TOTAL_IMPORTED_ENERGY, watt_hour_value, {persist = true}) - if ib.endpoint_id ~= 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) - else - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT), capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) - end + local total_imported_energy = device:get_field(TOTAL_IMPORTED_ENERGY) or {} + total_imported_energy[ib.endpoint_id] = watt_hour_value + device:set_field(TOTAL_IMPORTED_ENERGY, total_imported_energy, {persist = true}) + device:emit_component_event(component, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) end end local function per_energy_imported_handler(driver, device, ib, response) if ib.data.elements.energy then + local component = device.profile.components["main"] local watt_hour_value = ib.data.elements.energy.value / CONVERSION_CONST_MILLIWATT_TO_WATT - local latest_energy_report = device:get_field(TOTAL_IMPORTED_ENERGY) or 0 - local summed_energy_report = latest_energy_report + watt_hour_value - device:set_field(TOTAL_IMPORTED_ENERGY, summed_energy_report, {persist = true}) - device:emit_event(capabilities.energyMeter.energy({ value = summed_energy_report, unit = "Wh" })) + local total_imported_energy = device:get_field(TOTAL_IMPORTED_ENERGY) or {} + local latest_energy_report = total_imported_energy[ib.endpoint_id] or 0 + total_imported_energy[ib.endpoint_id] = latest_energy_report + watt_hour_value + device:set_field(TOTAL_IMPORTED_ENERGY, total_imported_energy, {persist = true}) + device:emit_component_event(component, capabilities.energyMeter.energy({ value = total_imported_energy[ib.endpoint_id], unit = "Wh" })) end end @@ -1195,14 +1276,12 @@ local function short_release_event_handler(driver, device, ib, response) end local function active_power_handler(driver, device, ib, response) + local component = device.profile.components["main"] if ib.data.value then local watt_value = ib.data.value / CONVERSION_CONST_MILLIWATT_TO_WATT - if ib.endpoint_id ~= 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - else - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT), capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - end + device:emit_component_event(component, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + else + device:emit_component_event(component, capabilities.powerMeter.power({ value = 0, unit = "W"})) end end @@ -1316,8 +1395,22 @@ local function device_added(driver, device) if device.network_type == device_lib.NETWORK_TYPE_CHILD then local req = clusters.OnOff.attributes.OnOff:read(device) device:send(req) + elseif device.network_type == device_lib.NETWORK_TYPE_MATTER then + collect_and_store_electrical_sensor_info(driver, device) + local electrical_sensor_eps = device:get_field(ELECTRICAL_SENSOR_EPS) + if #electrical_sensor_eps == 0 then + device:set_field(profiling_data.ELECTRICAL_TOPOLOGY, {topology = false, tags_on_ep = {}}) + device:set_field(ELECTRICAL_SENSOR_EPS, nil) + elseif electrical_sensor_eps[1].topology == clusters.PowerTopology.types.Feature.NODE_TOPOLOGY then + local switch_eps = device:get_endpoints(clusters.OnOff.ID) + table.sort(switch_eps) + local electrical_tags = "" + if electrical_sensor_eps[1].power then electrical_tags = electrical_tags .. "-power" end + if electrical_sensor_eps[1].energy then electrical_tags = electrical_tags .. "-energy-powerConsumption" end + device:set_field(profiling_data.ELECTRICAL_TOPOLOGY, {topology = clusters.PowerTopology.types.Feature.NODE_TOPOLOGY, tags_on_ep = {[switch_eps[1]] = electrical_tags}}) + device:set_field(ELECTRICAL_SENSOR_EPS, nil) + end end - -- call device init in case init is not called after added due to device caching device_init(driver, device) end @@ -1475,6 +1568,9 @@ local matter_driver_template = { [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = energy_report_handler_factory(true), [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = energy_report_handler_factory(false), }, + [clusters.PowerTopology.ID] = { + [clusters.PowerTopology.attributes.AvailableEndpoints.ID] = available_endpoints_handler, + }, [clusters.ValveConfigurationAndControl.ID] = { [clusters.ValveConfigurationAndControl.attributes.CurrentState.ID] = valve_state_attr_handler, [clusters.ValveConfigurationAndControl.attributes.CurrentLevel.ID] = valve_level_attr_handler 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 830a9b99f8..06c726854a 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 @@ -141,6 +141,7 @@ local function test_init() end test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) + aqara_mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({aqara_mock_device.id, read_attribute_list}) 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 369689e181..2e5aef1f2d 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 @@ -38,7 +38,8 @@ local aqara_mock_device = test.mock_device.build_test_matter_device({ clusters = { {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, {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 } + {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 5 }, + {cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 1 } -- NODE_TOPOLOGY }, device_types = { {device_type_id = 0x0016, device_type_revision = 1}, -- RootNode @@ -177,10 +178,12 @@ local function test_init() end end test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) + + -- Test added -> doConfigure logic + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) + test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) - test.mock_devices_api._expected_device_updates[aqara_mock_device.device_id] = "00000000-1111-2222-3333-000000000001" - test.mock_devices_api._expected_device_updates[1] = {device_id = "00000000-1111-2222-3333-000000000001"} - test.mock_devices_api._expected_device_updates[1].metadata = {deviceId="00000000-1111-2222-3333-000000000001", profileReference="4-button"} + aqara_mock_device:expect_metadata_update({ profile = "4-button" }) aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(aqara_mock_device) -- to test powerConsumptionReport @@ -277,7 +280,7 @@ test.register_coroutine_test( { -- don't use "aqara_mock_children[aqara_child1_ep].id," -- because energy management is at the root endpoint. - aqara_mock_device.id, + aqara_mock_children[aqara_child1_ep].id, clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_device, 1, 17000) } ) @@ -289,7 +292,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { - aqara_mock_device.id, + aqara_mock_children[aqara_child1_ep].id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_19) } ) @@ -302,7 +305,7 @@ test.register_coroutine_test( -- This is because related variable settings are required in set_poll_report_timer_and_schedule(). test.socket.matter:__queue_receive( { - aqara_mock_device.id, + aqara_mock_children[aqara_child1_ep].id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_29) } ) @@ -313,10 +316,8 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { - aqara_mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - aqara_mock_device, 1, cumulative_report_val_39 - ) + aqara_mock_children[aqara_child1_ep].id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_39) } ) 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 e812bdfdaa..4f88338185 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -15,6 +15,7 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" local clusters = require "st.matter.clusters" @@ -42,6 +43,7 @@ local mock_device = test.mock_device.build_test_matter_device({ clusters = { { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, { cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", feature_map = 0, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY }, device_types = { { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor @@ -51,10 +53,30 @@ local mock_device = test.mock_device.build_test_matter_device({ endpoint_id = 2, clusters = { { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + { 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 }, -- OnOff Dimmable Plug + } + }, + { + endpoint_id = 3, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + } + }, + { + endpoint_id = 4, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + { cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + }, + device_types = { + { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug } }, }, @@ -80,16 +102,20 @@ local mock_device_periodic = test.mock_device.build_test_matter_device({ { endpoint_id = 1, clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 10, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, } -- SET_TOPOLOGY }, device_types = { - { device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor + { device_type_id = 0x010A, device_type_revision = 1 }, -- OnOff Plug + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor } }, }, }) local subscribed_attributes_periodic = { + clusters.OnOff.attributes.OnOff, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, } @@ -643,9 +669,30 @@ test.register_coroutine_test( test.register_coroutine_test( "Test profile change on init for Electrical Sensor device type", function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + local read_req = clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device.id, 1) + read_req:merge(clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device.id, 3)) + test.socket.matter:__expect_send({ mock_device.id, read_req }) + local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) + for i, cluster in ipairs(subscribed_attributes) 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.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 1, {uint32(2)})}) + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 3, {uint32(4)})}) + mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "nil 2", + profile = "plug-level-energy-powerConsumption", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 4) + }) end, { test_init = test_init } ) @@ -653,9 +700,21 @@ test.register_coroutine_test( test.register_coroutine_test( "Test profile change on init for only Periodic Electrical Sensor device type", function() + test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "added" }) + local read_req = clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device_periodic.id, 1) + test.socket.matter:__expect_send({ mock_device_periodic.id, read_req }) + local subscribe_request = subscribed_attributes_periodic[1]:subscribe(mock_device_periodic) + for i, cluster in ipairs(subscribed_attributes_periodic) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_periodic)) + end + end + test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "doConfigure" }) - mock_device_periodic:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) mock_device_periodic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device_periodic.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device_periodic, 1, {uint32(1)})}) + mock_device_periodic:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) end, { test_init = test_init_periodic } ) @@ -692,7 +751,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 2) + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 1) } }, { 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 f34f432ec7..8d10daea11 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -63,6 +63,7 @@ local function test_init() if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua index 7d3211bdfb..b211bf8f53 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua @@ -90,6 +90,7 @@ local function test_init() end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) + mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ profile = "light-color-level-fan" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 624a3ab205..7a5c39d4c2 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 @@ -122,6 +122,7 @@ local function test_init() if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device) 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 139c519dbf..3c0c32037a 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 @@ -198,6 +198,7 @@ local function test_init() if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ profile = "light-level-3-button" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -236,6 +237,7 @@ local function test_init_mcd_unsupported_switch_device_type() end end test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) + mock_device_mcd_unsupported_switch_device_type:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "doConfigure" }) mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ profile = "2-button" }) mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 6debff2163..e6be37a41c 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 @@ -366,6 +366,7 @@ local function test_init_parent_child_switch_types() local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_parent_child_switch_types) test.socket.matter:__expect_send({mock_device_parent_child_switch_types.id, subscribe_request}) + mock_device_parent_child_switch_types:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "doConfigure" }) mock_device_parent_child_switch_types:expect_metadata_update({ profile = "switch-level" }) mock_device_parent_child_switch_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -383,6 +384,7 @@ end local function test_init_onoff() test.mock_device.add_test_device(mock_device_onoff) + mock_device_onoff:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "doConfigure" }) mock_device_onoff:expect_metadata_update({ profile = "switch-binary" }) mock_device_onoff:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -395,6 +397,7 @@ end local function test_init_parent_client_child_server() local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_parent_client_child_server) test.socket.matter:__expect_send({mock_device_parent_client_child_server.id, subscribe_request}) + mock_device_parent_client_child_server:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "doConfigure" }) mock_device_parent_client_child_server:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_client_child_server:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -403,6 +406,7 @@ end local function test_init_dimmer() test.mock_device.add_test_device(mock_device_dimmer) + mock_device_dimmer:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "doConfigure" }) mock_device_dimmer:expect_metadata_update({ profile = "switch-level" }) mock_device_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -410,6 +414,7 @@ end local function test_init_color_dimmer() test.mock_device.add_test_device(mock_device_color_dimmer) + mock_device_color_dimmer:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "doConfigure" }) mock_device_color_dimmer:expect_metadata_update({ profile = "switch-color-level" }) mock_device_color_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -426,7 +431,9 @@ local function test_init_mounted_on_off_control() end end test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) + mock_device_mounted_on_off_control:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. 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" }) test.mock_device.add_test_device(mock_device_mounted_on_off_control) end @@ -442,13 +449,16 @@ local function test_init_mounted_dimmable_load_control() end end test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) + mock_device_mounted_dimmable_load_control:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. 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" }) test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) end local function test_init_water_valve() test.mock_device.add_test_device(mock_device_water_valve) + mock_device_water_valve:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "doConfigure" }) mock_device_water_valve:expect_metadata_update({ profile = "water-valve-level" }) mock_device_water_valve:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -476,7 +486,9 @@ local function test_init_parent_child_different_types() end test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) + mock_device_parent_child_different_types:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. 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" }) test.mock_device.add_test_device(mock_device_parent_child_different_types) @@ -491,6 +503,7 @@ local function test_init_parent_child_different_types() end local function test_init_parent_child_unsupported_device_type() + mock_device_parent_child_unsupported_device_type:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "doConfigure" }) mock_device_parent_child_unsupported_device_type:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_child_unsupported_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 de62865597..66f019cdb0 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 @@ -180,7 +180,9 @@ local function test_init() end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. 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" }) test.mock_device.add_test_device(mock_device) @@ -246,7 +248,9 @@ local function test_init_parent_child_endpoints_non_sequential() end test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) + mock_device_parent_child_endpoints_non_sequential:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. 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" }) test.mock_device.add_test_device(mock_device_parent_child_endpoints_non_sequential) 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 cbf8fb0afe..a69cdfeda7 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 @@ -23,6 +23,8 @@ local child_profile_override = t_utils.get_profile_definition("switch-binary.yml local parent_ep = 10 local child1_ep = 20 local child2_ep = 30 +local child3_ep = 40 +local child4_ep = 50 local mock_device = test.mock_device.build_test_matter_device({ label = "Matter Switch", @@ -68,6 +70,24 @@ local mock_device = test.mock_device.build_test_matter_device({ {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug } }, + { + endpoint_id = child3_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + } + }, + { + endpoint_id = child4_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + } + } } }) @@ -138,7 +158,9 @@ local function test_init() local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. 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" }) test.mock_device.add_test_device(mock_device) @@ -161,6 +183,22 @@ local function test_init() parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", child2_ep) }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 4", + profile = "plug-binary", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", child3_ep) + }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 5", + profile = "plug-binary", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", child4_ep) + }) end local mock_children_child_profile_override = {} @@ -183,7 +221,9 @@ local function test_init_child_profile_override() local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_child_profile_override) test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) + mock_device_child_profile_override:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. 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" }) test.mock_device.add_test_device(mock_device_child_profile_override)