diff --git a/drivers/SmartThings/matter-sensor/profiles/co-battery.yml b/drivers/SmartThings/matter-sensor/profiles/co-battery.yml new file mode 100644 index 0000000000..25d1bb7f03 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/co-battery.yml @@ -0,0 +1,16 @@ +name: co-battery +components: +- id: main + capabilities: + - id: carbonMonoxideDetector + version: 1 + - id: battery + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmokeDetector \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/profiles/co-comeas-battery.yml b/drivers/SmartThings/matter-sensor/profiles/co-comeas-battery.yml new file mode 100644 index 0000000000..4c8f554572 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/co-comeas-battery.yml @@ -0,0 +1,18 @@ +name: co-comeas-battery +components: +- id: main + capabilities: + - id: carbonMonoxideDetector + version: 1 + - id: carbonMonoxideMeasurement + version: 1 + - id: battery + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmokeDetector \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/profiles/smoke-battery.yml b/drivers/SmartThings/matter-sensor/profiles/smoke-battery.yml new file mode 100644 index 0000000000..8abff2f38e --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/smoke-battery.yml @@ -0,0 +1,19 @@ +name: smoke-battery +components: +- id: main + capabilities: + - id: smokeDetector + version: 1 + - id: battery + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmokeDetector +preferences: + - preferenceId: certifiedpreferences.smokeSensorSensitivity + explicit: true \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/profiles/smoke-co-battery.yml b/drivers/SmartThings/matter-sensor/profiles/smoke-co-battery.yml new file mode 100644 index 0000000000..96d036d131 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/smoke-co-battery.yml @@ -0,0 +1,21 @@ +name: smoke-co-battery +components: +- id: main + capabilities: + - id: smokeDetector + version: 1 + - id: carbonMonoxideDetector + version: 1 + - id: battery + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmokeDetector +preferences: + - preferenceId: certifiedpreferences.smokeSensorSensitivity + explicit: true \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/profiles/smoke-co-comeas-battery.yml b/drivers/SmartThings/matter-sensor/profiles/smoke-co-comeas-battery.yml new file mode 100644 index 0000000000..795594d8a4 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/smoke-co-comeas-battery.yml @@ -0,0 +1,23 @@ +name: smoke-co-comeas-battery +components: +- id: main + capabilities: + - id: smokeDetector + version: 1 + - id: carbonMonoxideDetector + version: 1 + - id: carbonMonoxideMeasurement + version: 1 + - id: battery + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmokeDetector +preferences: + - preferenceId: certifiedpreferences.smokeSensorSensitivity + explicit: true \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/profiles/smoke-co-temp-humidity-comeas-battery.yml b/drivers/SmartThings/matter-sensor/profiles/smoke-co-temp-humidity-comeas-battery.yml new file mode 100644 index 0000000000..c7c3b3c00b --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/smoke-co-temp-humidity-comeas-battery.yml @@ -0,0 +1,31 @@ +name: smoke-co-temp-humidity-comeas-battery +components: +- id: main + capabilities: + - id: smokeDetector + version: 1 + - id: carbonMonoxideDetector + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: temperatureMeasurement + version: 1 + - id: carbonMonoxideMeasurement + version: 1 + - id: battery + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmokeDetector +preferences: + - preferenceId: certifiedpreferences.smokeSensorSensitivity + explicit: true + - preferenceId: tempOffset + explicit: true + - preferenceId: humidityOffset + explicit: true \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua b/drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua index 67799aa780..fc5bd3f167 100644 --- a/drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua +++ b/drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua @@ -20,6 +20,13 @@ local CARBON_MONOXIDE_MEASUREMENT_UNIT = "CarbonMonoxideConcentrationMeasurement local SMOKE_CO_ALARM_DEVICE_TYPE_ID = 0x0076 local PROFILE_MATCHED = "__profile_matched" +local HUE_MANUFACTURER_ID = 0x100B + +local battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" +} + local version = require "version" if version.api < 10 then clusters.SmokeCoAlarm = require "SmokeCoAlarm" @@ -49,13 +56,18 @@ end local supported_profiles = { "co", + "co-battery", "co-comeas", + "co-comeas-battery", "smoke", + "smoke-battery", "smoke-co-comeas", - "smoke-co-temp-humidity-comeas" + "smoke-co-comeas-battery", + "smoke-co-temp-humidity-comeas", + "smoke-co-temp-humidity-comeas-battery" } -local function match_profile(device) +local function match_profile(device, battery_supported) local smoke_eps = embedded_cluster_utils.get_endpoints(device, clusters.SmokeCoAlarm.ID, {feature_bitmap = clusters.SmokeCoAlarm.types.Feature.SMOKE_ALARM}) local co_eps = embedded_cluster_utils.get_endpoints(device, clusters.SmokeCoAlarm.ID, {feature_bitmap = clusters.SmokeCoAlarm.types.Feature.CO_ALARM}) local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) @@ -84,6 +96,9 @@ local function match_profile(device) if #co_level_eps > 0 then profile_name = profile_name .. "-colevel" end + if battery_supported == battery_support.BATTERY_PERCENTAGE then + profile_name = profile_name .. "-battery" + end -- remove leading "-" profile_name = string.sub(profile_name, 2) @@ -108,7 +123,13 @@ end local function device_init(driver, device) if not device:get_field(PROFILE_MATCHED) then - match_profile(device) + local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) + -- Hue devices support the PowerSource cluster but don't support reporting battery percentage remaining + if #battery_feature_eps > 0 and device.manufacturer_info.vendor_id ~= HUE_MANUFACTURER_ID then + device:send(clusters.PowerSource.attributes.AttributeList:read()) + else + match_profile(device, battery_support.NO_BATTERY) + end end device:subscribe() end @@ -201,6 +222,16 @@ local function battery_alert_attr_handler(driver, device, ib, response) end end +local function power_source_attribute_list_handler(driver, device, ib, response) + for _, attr in ipairs(ib.data.elements) do + -- Re-profile the device if BatPercentRemaining is available + if attr.value == clusters.PowerSource.attributes.BatPercentRemaining.ID then + match_profile(device, battery_support.BATTERY_PERCENTAGE) + return + end + end +end + local matter_smoke_co_alarm_handler = { NAME = "matter-smoke-co-alarm", lifecycle_handlers = { @@ -219,7 +250,10 @@ local matter_smoke_co_alarm_handler = { [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = carbon_monoxide_attr_handler, [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = carbon_monoxide_unit_attr_handler, - } + }, + [clusters.PowerSource.ID] = { + [clusters.PowerSource.attributes.AttributeList.ID] = power_source_attribute_list_handler, + }, }, }, subscribed_attributes = { @@ -246,6 +280,9 @@ local matter_smoke_co_alarm_handler = { }, [capabilities.batteryLevel.ID] = { clusters.SmokeCoAlarm.attributes.BatteryAlert, + }, + [capabilities.battery.ID] = { + clusters.PowerSource.attributes.BatPercentRemaining, } }, can_handle = is_matter_smoke_co_alarm diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm.lua index 527701bf41..33d28d4b00 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm.lua @@ -73,6 +73,8 @@ local cluster_subscribe_list = { } local function test_init() + local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() + test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -81,7 +83,6 @@ 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:expect_metadata_update({ profile = "smoke-co-temp-humidity-comeas" }) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm_battery.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm_battery.lua new file mode 100644 index 0000000000..66bd3bc4a9 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm_battery.lua @@ -0,0 +1,131 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" + +local clusters = require "st.matter.clusters" +clusters.SmokeCoAlarm = require "SmokeCoAlarm" +local version = require "version" +if version.api < 10 then + clusters.SmokeCoAlarm = require "SmokeCoAlarm" + clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" +end + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("smoke-co-temp-humidity-comeas-battery.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.SmokeCoAlarm.ID, cluster_type = "SERVER", feature_map = clusters.SmokeCoAlarm.types.Feature.CO_ALARM | clusters.SmokeCoAlarm.types.Feature.SMOKE_ALARM}, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.CarbonMonoxideConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = clusters.CarbonMonoxideConcentrationMeasurement.types.Feature.NUMERIC_MEASUREMENT}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY}, + }, + device_types = { + {device_type_id = 0x0076, device_type_revision = 1} -- Smoke CO Alarm + } + } + } +}) + +local cluster_subscribe_list = { + clusters.SmokeCoAlarm.attributes.SmokeState, + clusters.SmokeCoAlarm.attributes.TestInProgress, + clusters.SmokeCoAlarm.attributes.COState, + clusters.SmokeCoAlarm.attributes.HardwareFaultAlert, + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, + clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, + clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, + clusters.PowerSource.attributes.BatPercentRemaining, +} + +local function test_init() + local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() + test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Test profile change when battery percent remaining attribute (attribute ID 12) is available", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32(12)}) + } + ) + mock_device:expect_metadata_update({ profile = "smoke-co-temp-humidity-comeas-battery" }) + end +) + +test.register_coroutine_test( + "Test that profile does not change when battery percent remaining attribute is not available", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32(0)}) + } + ) + end +) + +test.register_coroutine_test( + "Battery percent reports should generate correct messages", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data(mock_device, 1, 150) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5)) + ) + ) + end +) + +test.run_registered_tests()