Skip to content

Matter Sensor: Add modular profile supports for AQS #2082

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
58 changes: 58 additions & 0 deletions drivers/SmartThings/matter-sensor/profiles/aqs-modular.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: aqs-modular
components:
- id: main
capabilities:
- id: airQualityHealthConcern
- id: temperatureMeasurement
optional: true
- id: relativeHumidityMeasurement
optional: true
- id: carbonMonoxideMeasurement
optional: true
- id: carbonMonoxideHealthConcern
optional: true
- id: carbonDioxideMeasurement
optional: true
- id: carbonDioxideHealthConcern
optional: true
- id: nitrogenDioxideMeasurement
optional: true
- id: ozoneMeasurement
optional: true
- id: formaldehydeMeasurement
optional: true
- id: formaldehydeHealthConcern
optional: true
- id: veryFineDustSensor
optional: true
- id: veryFineDustHealthConcern
optional: true
- id: fineDustHealthConcern
optional: true
- id: dustSensor
optional: true
- id: dustHealthConcern
optional: true
- id: radonMeasurement
optional: true
- id: tvocMeasurement
optional: true
- id: firmwareUpdate
- id: refresh
- id: nitrogenDioxideHealthConcern
optional: true
- id: ozoneHealthConcern
optional: true
- id: radonHealthConcern
optional: true
- id: tvocHealthConcern
optional: true
- id: fineDustSensor
optional: true
categories:
- name: AirQualityDetector
preferences:
- preferenceId: tempOffset
explicit: true
- preferenceId: humidityOffset
explicit: true
111 changes: 101 additions & 10 deletions drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ local embedded_cluster_utils = require "embedded-cluster-utils"
local log = require "log"
local AIR_QUALITY_SENSOR_DEVICE_TYPE_ID = 0x002C

local SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities"


-- Include driver-side definitions when lua libs api version is < 10
local version = require "version"
if version.api < 10 then
Expand Down Expand Up @@ -141,10 +144,6 @@ local units_required = {
clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement
}

local function device_init(driver, device)
device:subscribe()
end

local tbl_contains = function(t, val)
for _, v in pairs(t) do
if v == val then
Expand Down Expand Up @@ -210,15 +209,33 @@ local function create_level_measurement_profile(device)
return meas_name, level_name
end

local function do_configure(driver, device)
local function supported_level_measurements(device)
local measurement_caps, level_caps = {}, {}
for _, details in ipairs(AIR_QUALITY_MAP) do
local cap_id = details[1]
local cluster = details[3]
-- capability describes either a HealthConcern or Measurement/Sensor
if (cap_id:match("HealthConcern$")) then
local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION })
if #attr_eps > 0 then
device.log.info(string.format("Adding %s cap to table", cap_id))
table.insert(level_caps, cap_id)
end
elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then
local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT })
if #attr_eps > 0 then
device.log.info(string.format("Adding %s cap to table", cap_id))
table.insert(measurement_caps, cap_id)
end
end
end
return measurement_caps, level_caps
end

local function match_profile_switch(driver, device)
local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID)
local humidity_eps = embedded_cluster_utils.get_endpoints(device, clusters.RelativeHumidityMeasurement.ID)

-- we have to read the unit before reports of values will do anything
for _, cluster in ipairs(units_required) do
device:send(cluster.attributes.MeasurementUnit:read(device))
end

local profile_name = "aqs"

if #temp_eps > 0 then
Expand Down Expand Up @@ -271,6 +288,80 @@ local function do_configure(driver, device)
device:try_update_metadata({profile = profile_name})
end

local function supports_capability_by_id_modular(device, capability, component)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: do we need the _modular at the end of this function? Does its name need to be distinct from the scripting engine function it's overwriting?

for _, component_capabilities in ipairs(device:get_field(SUPPORTED_COMPONENT_CAPABILITIES)) do
local comp_id = component_capabilities[1]
local capability_ids = component_capabilities[2]
if (component == nil) or (component == comp_id) then
for _, cap in ipairs(capability_ids) do
if cap == capability then
return true
end
end
end
end
return false
end

local function match_modular_profile(driver, device)
local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID)
local humidity_eps = embedded_cluster_utils.get_endpoints(device, clusters.RelativeHumidityMeasurement.ID)

local optional_supported_component_capabilities = {}
local main_component_capabilities = {}

if #temp_eps > 0 then
table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID)
end
if #humidity_eps > 0 then
table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID)
end

local measurement_caps, level_caps = supported_level_measurements(device)

for _, cap_id in ipairs(measurement_caps) do
table.insert(main_component_capabilities, cap_id)
end

for _, cap_id in ipairs(level_caps) do
table.insert(main_component_capabilities, cap_id)
end

table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities})

device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, optional_supported_component_capabilities)
Copy link
Contributor

Choose a reason for hiding this comment

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

this line does not seem required, since the field is overwritten two lines later


device:try_update_metadata({profile = "aqs-modular", optional_component_capabilities = optional_supported_component_capabilities})

-- add mandatory capabilities for subscription
local total_supported_capabilities = optional_supported_component_capabilities
table.insert(total_supported_capabilities[1][2], capabilities.airQualityHealthConcern.ID)
Copy link
Contributor

@hcarter-775 hcarter-775 Apr 18, 2025

Choose a reason for hiding this comment

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

nit, though I think inserting into "[1][2]" is a little confusing and asks the reader to trace back what [1][2] actually references.


device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true })

--re-up subscription with new capabiltiies using the moudlar supports_capability override
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
--re-up subscription with new capabiltiies using the moudlar supports_capability override
--re-up subscription with new capabilities using the modular supports_capability override

device:extend_device("supports_capability_by_id", supports_capability_by_id_modular)
device:subscribe()
end

local function do_configure(driver, device)
-- must use profile switching on older hubs
if version.api < 14 and version.rpc < 7 then
Copy link
Contributor

Choose a reason for hiding this comment

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

Was the RPC version bumped for 0.57?

match_profile_switch(driver, device)
else
match_modular_profile(driver, device)
end
end

local function device_init(driver, device)
if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then
-- assume that device is using a modular profile, override supports_capability_by_id
-- library function to utilize optional capabilities
device:extend_device("supports_capability_by_id", supports_capability_by_id_modular)
end
device:subscribe()
end

local function store_unit_factory(capability_name)
return function(driver, device, ib, response)
device:set_field(capability_name.."_unit", ib.data.value, {persist = true})
Expand Down
Loading