Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion drivers/SmartThings/matter-switch/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ function SwitchLifecycleHandlers.device_added(driver, device)
device:send(clusters.OnOff.attributes.OnOff:read(device))
elseif device.network_type == device_lib.NETWORK_TYPE_MATTER then
switch_utils.handle_electrical_sensor_info(device)
if #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) == 0 then
device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist = true})
end
end

-- call device init in case init is not called after added due to device caching
Expand Down Expand Up @@ -94,7 +97,7 @@ function SwitchLifecycleHandlers.device_init(driver, device)
device:extend_device("subscribe", switch_utils.subscribe)
device:subscribe()

-- device energy reporting must be handled cumulatively, periodically, or by both simulatanously.
-- device energy reporting must be handled cumulatively, periodically, or by both simultaneously.
-- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported.
if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID,
{feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY}) > 0 then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,10 +319,10 @@ function AttributeHandlers.available_endpoints_handler(driver, device, ib, respo
local set_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS)
for i, set_ep_info in pairs(set_topology_eps or {}) do
if ib.endpoint_id == set_ep_info.endpoint_id then
-- since EP reponse is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table
-- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table
switch_utils.remove_field_index(device, fields.ELECTRICAL_SENSOR_EPS, i)
local available_endpoints_ids = {}
for _, element in pairs(ib.data.elements) do
for _, element in pairs(ib.data.elements or {}) do
table.insert(available_endpoints_ids, element.value)
end
-- set the required profile elements ("-power", etc.) to one of these EP IDs for later profiling.
Expand All @@ -344,10 +344,10 @@ function AttributeHandlers.parts_list_handler(driver, device, ib, response)
local tree_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS)
for i, tree_ep_info in pairs(tree_topology_eps or {}) do
if ib.endpoint_id == tree_ep_info.endpoint_id then
-- since EP reponse is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table
-- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table
switch_utils.remove_field_index(device, fields.ELECTRICAL_SENSOR_EPS, i)
local associated_endpoints_ids = {}
for _, element in pairs(ib.data.elements) do
for _, element in pairs(ib.data.elements or {}) do
table.insert(associated_endpoints_ids, element.value)
end
-- set the required profile elements ("-power", etc.) to one of these EP IDs for later profiling.
Expand Down Expand Up @@ -382,29 +382,19 @@ function AttributeHandlers.bat_charge_level_handler(driver, device, ib, response
end

function AttributeHandlers.power_source_attribute_list_handler(driver, device, ib, response)
local profile_name = ""

local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
for _, attr in ipairs(ib.data.elements) do
-- Re-profile the device if BatPercentRemaining (Attribute ID 0x0C) or
-- BatChargeLevel (Attribute ID 0x0E) is present.
if attr.value == 0x0C then
profile_name = "button-battery"
break
elseif attr.value == 0x0E then
profile_name = "button-batteryLevel"
local previous_battery_support = device:get_field(fields.profiling_data.BATTERY_SUPPORT)
device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist=true})
for _, attr in ipairs(ib.data.elements or {}) do
if attr.value == clusters.PowerSource.attributes.BatPercentRemaining.ID then
device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_PERCENTAGE, {persist=true})
break
elseif attr.value == clusters.PowerSource.attributes.BatChargeLevel.ID and
device:get_field(fields.profiling_data.BATTERY_SUPPORT) ~= fields.battery_support.BATTERY_PERCENTAGE then -- don't overwrite if percentage support is already detected
device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_LEVEL, {persist=true})
end
end
if profile_name ~= "" then
if #button_eps > 1 then
profile_name = string.format("%d-", #button_eps) .. profile_name
end

if switch_utils.get_product_override_field(device, "is_climate_sensor_w100") then
profile_name = profile_name .. "-temperature-humidity"
end
device:try_update_metadata({ profile = profile_name })
if not previous_battery_support or previous_battery_support ~= device:get_field(fields.profiling_data.BATTERY_SUPPORT) then
device_cfg.match_profile(driver, device)
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,16 @@ function ButtonDeviceConfiguration.update_button_profile(device, default_endpoin
if #motion_eps > 0 and (num_button_eps == 3 or num_button_eps == 6) then -- only these two devices are handled
profile_name = profile_name .. "-motion"
end
local battery_supported = #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) > 0
if battery_supported then -- battery profiles are configured later, in power_source_attribute_list_handler
device:send(clusters.PowerSource.attributes.AttributeList:read(device))
else
device:try_update_metadata({profile = profile_name})
local battery_support = device:get_field(fields.profiling_data.BATTERY_SUPPORT)
if battery_support == fields.battery_support.BATTERY_PERCENTAGE then
profile_name = profile_name .. "-battery"
elseif battery_support == fields.battery_support.BATTERY_LEVEL then
profile_name = profile_name .. "-batteryLevel"
end
if switch_utils.get_product_override_field(device, "is_climate_sensor_w100") then
profile_name = "3-button-battery-temperature-humidity"
end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that this is the only unique handling needed for this device (plus the existing logic in find_default_endpoint), so I was able to remove the extra block in power_source_attribute_list_handler

return profile_name
end

function ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, button_eps)
Expand Down Expand Up @@ -238,13 +242,12 @@ function DeviceConfiguration.match_profile(driver, device)
end

-- initialize the main device card with buttons if applicable
local momemtary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #momemtary_switch_ep_ids) then
ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #momemtary_switch_ep_ids)
local momentary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #momentary_switch_ep_ids) then
updated_profile = ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #momentary_switch_ep_ids)
-- 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, momemtary_switch_ep_ids)
ButtonDeviceConfiguration.configure_buttons(device, momemtary_switch_ep_ids)
return
ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, momentary_switch_ep_ids)
ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep_ids)
end

device:try_update_metadata({ profile = updated_profile, optional_component_capabilities = optional_component_capabilities })
Expand All @@ -254,4 +257,4 @@ return {
DeviceCfg = DeviceConfiguration,
SwitchCfg = SwitchDeviceConfiguration,
ButtonCfg = ButtonDeviceConfiguration
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,20 @@ SwitchFields.switch_category_vendor_overrides = {
SwitchFields.ELECTRICAL_SENSOR_EPS = "__electrical_sensor_eps"

--- used in tandem with an EP ID. Stores the required electrical tags "-power", "-energy-powerConsumption", etc.
--- for an Electrical Sensor EP with a "primary" endpoint, used during device profling.
--- for an Electrical Sensor EP with a "primary" endpoint, used during device profiling.
SwitchFields.ELECTRICAL_TAGS = "__electrical_tags"

SwitchFields.MODULAR_PROFILE_UPDATED = "__modular_profile_updated"

SwitchFields.profiling_data = {
POWER_TOPOLOGY = "__power_topology",
BATTERY_SUPPORT = "__battery_support",
}

SwitchFields.battery_support = {
NO_BATTERY = "NO_BATTERY",
BATTERY_LEVEL = "BATTERY_LEVEL",
BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE",
}

SwitchFields.ENERGY_METER_OFFSET = "__energy_meter_offset"
Expand Down
7 changes: 7 additions & 0 deletions drivers/SmartThings/matter-switch/src/switch_utils/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,13 @@ function utils.subscribe(device)
end
end

-- If the type of battery support has not yet been determined, add the PowerSource AttributeList to the list of
-- subscribed attributes in order to determine which if any battery capability should be used.
if device:get_field(fields.profiling_data.BATTERY_SUPPORT) == nil then
local ib = im.InteractionInfoBlock(nil, clusters.PowerSource.ID, clusters.PowerSource.attributes.AttributeList.ID)
subscribe_request:with_info_block(ib)
end

if #subscribe_request.info_blocks > 0 then
device:send(subscribe_request)
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
-- Copyright © 2024 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local test = require "integration_test"
local t_utils = require "integration_test.utils"
local capabilities = require "st.capabilities"
local utils = require "st.utils"
local dkjson = require "dkjson"
local uint32 = require "st.matter.data_types.Uint32"
local clusters = require "st.matter.generated.zap_clusters"
local button_attr = capabilities.button.button
local t_utils = require "integration_test.utils"
local test = require "integration_test"
local uint32 = require "st.matter.data_types.Uint32"

-- Mock a 3-button device with temperature and humidity sensor
local aqara_mock_device = test.mock_device.build_test_matter_device({
Expand Down Expand Up @@ -100,19 +97,20 @@ local aqara_mock_device = test.mock_device.build_test_matter_device({

local function configure_buttons()
test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 3)})
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", button_attr.pushed({state_change = false})))
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", capabilities.button.button.pushed({state_change = false})))

test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 4)})
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", button_attr.pushed({state_change = false})))
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.button.pushed({state_change = false})))

test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 5)})
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", button_attr.pushed({state_change = false})))
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", capabilities.button.button.pushed({state_change = false})))
end

local function test_init()
test.disable_startup_messages()
test.mock_device.add_test_device(aqara_mock_device)
local cluster_subscribe_list = {
clusters.PowerSource.server.attributes.AttributeList,
clusters.PowerSource.server.attributes.BatPercentRemaining,
clusters.TemperatureMeasurement.attributes.MeasuredValue,
clusters.TemperatureMeasurement.attributes.MinMeasuredValue,
Expand All @@ -138,23 +136,16 @@ local function test_init()
test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request})

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})
configure_buttons()
aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })

local device_info_copy = utils.deep_copy(aqara_mock_device.raw_st_data)
device_info_copy.profile.id = "3-button-battery-temperature-humidity"
local device_info_json = dkjson.encode(device_info_copy)
test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "infoChanged", device_info_json })
test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request})
configure_buttons()
end

test.set_test_init_function(test_init)

local function update_profile()
test.socket.matter:__queue_receive({aqara_mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data(aqara_mock_device, 6, {uint32(0x0C)})})
test.socket.matter:__queue_receive({aqara_mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data(
aqara_mock_device, 6, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)}
)})
configure_buttons()
aqara_mock_device:expect_metadata_update({ profile = "3-button-battery-temperature-humidity" })
end

Expand Down Expand Up @@ -315,7 +306,7 @@ test.register_coroutine_test(
clusters.Switch.events.MultiPressComplete:build_test_event_report(aqara_mock_device, 4, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1})
}
)
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", button_attr.double({state_change = true})))
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.button.double({state_change = true})))
end
)

Expand All @@ -335,7 +326,7 @@ test.register_coroutine_test(
clusters.Switch.events.LongPress:build_test_event_report(aqara_mock_device, 3, {new_position = 1})
}
)
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", button_attr.held({state_change = true})))
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", capabilities.button.button.held({state_change = true})))
test.socket.matter:__queue_receive(
{
aqara_mock_device.id,
Expand All @@ -361,7 +352,7 @@ test.register_coroutine_test(
clusters.Switch.events.LongPress:build_test_event_report(aqara_mock_device, 5, {new_position = 1})
}
)
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", button_attr.held({state_change = true})))
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", capabilities.button.button.held({state_change = true})))
test.socket.matter:__queue_receive(
{
aqara_mock_device.id,
Expand All @@ -382,7 +373,7 @@ test.register_coroutine_test(
}
)
test.socket.capability:__expect_send(
aqara_mock_device:generate_test_message("button1", button_attr.double({state_change = true}))
aqara_mock_device:generate_test_message("button1", capabilities.button.button.double({state_change = true}))
)
end
)
Expand Down Expand Up @@ -466,4 +457,3 @@ test.register_coroutine_test(
)

test.run_registered_tests()

Original file line number Diff line number Diff line change
Expand Up @@ -81,58 +81,45 @@ local function set_color_mode(device, endpoint, color_mode)
test.socket.matter:__expect_send({device.id, read_req})
end

local function test_init()
local cluster_subscribe_list = {
clusters.OnOff.attributes.OnOff,
clusters.LevelControl.attributes.CurrentLevel,
clusters.LevelControl.attributes.MaxLevel,
clusters.LevelControl.attributes.MinLevel,
clusters.ColorControl.attributes.CurrentHue,
clusters.ColorControl.attributes.CurrentSaturation,
clusters.ColorControl.attributes.CurrentX,
clusters.ColorControl.attributes.CurrentY,
clusters.ColorControl.attributes.ColorMode,
clusters.ColorControl.attributes.ColorTemperatureMireds,
clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds,
clusters.ColorControl.attributes.ColorTempPhysicalMinMireds,
clusters.IlluminanceMeasurement.attributes.MeasuredValue,
clusters.OccupancySensing.attributes.Occupancy
}
local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device)
for i, cluster in ipairs(cluster_subscribe_list) do
if i > 1 then
subscribe_request:merge(cluster:subscribe(mock_device))
end
local cluster_subscribe_list = {
clusters.OnOff.attributes.OnOff,
clusters.LevelControl.attributes.CurrentLevel,
clusters.LevelControl.attributes.MaxLevel,
clusters.LevelControl.attributes.MinLevel,
clusters.ColorControl.attributes.CurrentHue,
clusters.ColorControl.attributes.CurrentSaturation,
clusters.ColorControl.attributes.CurrentX,
clusters.ColorControl.attributes.CurrentY,
clusters.ColorControl.attributes.ColorMode,
clusters.ColorControl.attributes.ColorTemperatureMireds,
clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds,
clusters.ColorControl.attributes.ColorTempPhysicalMinMireds,
clusters.IlluminanceMeasurement.attributes.MeasuredValue,
clusters.OccupancySensing.attributes.Occupancy
}

local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device)
for i, cluster in ipairs(cluster_subscribe_list) do
if i > 1 then
subscribe_request:merge(cluster:subscribe(mock_device))
end
end

local function test_init()
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
test.socket.matter:__expect_send({mock_device.id, subscribe_request})

-- the following subscribe is due to the init event sent by the test framework.
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
test.mock_device.add_test_device(mock_device)
set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION)
end
test.set_test_init_function(test_init)

local function test_init_x_y_color_mode()
local cluster_subscribe_list = {
clusters.OnOff.attributes.OnOff,
clusters.LevelControl.attributes.CurrentLevel,
clusters.LevelControl.attributes.MaxLevel,
clusters.LevelControl.attributes.MinLevel,
clusters.ColorControl.attributes.CurrentHue,
clusters.ColorControl.attributes.CurrentSaturation,
clusters.ColorControl.attributes.CurrentX,
clusters.ColorControl.attributes.CurrentY,
clusters.ColorControl.attributes.ColorMode,
clusters.ColorControl.attributes.ColorTemperatureMireds,
clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds,
clusters.ColorControl.attributes.ColorTempPhysicalMinMireds,
clusters.IlluminanceMeasurement.attributes.MeasuredValue,
clusters.OccupancySensing.attributes.Occupancy
}
local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device)
for i, cluster in ipairs(cluster_subscribe_list) do
if i > 1 then
subscribe_request:merge(cluster:subscribe(mock_device))
end
end
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
test.socket.matter:__expect_send({mock_device.id, subscribe_request})

test.socket.matter:__expect_send({mock_device.id, subscribe_request})
test.mock_device.add_test_device(mock_device)
set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY)
Expand Down
Loading
Loading