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
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
-- Copyright © 2025 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local st_utils = require "st.utils"
local capabilities = require "st.capabilities"
local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields"

local AirQualityServerAttributeHandlers = {}


-- [[ GENERIC CONCENTRATION MEASUREMENT CLUSTER ATTRIBUTES ]]

function AirQualityServerAttributeHandlers.measurement_unit_factory(capability_name)
return function(driver, device, ib, response)
device:set_field(capability_name.."_unit", ib.data.value, {persist = true})
end
end

function AirQualityServerAttributeHandlers.level_value_factory(attribute)
return function(driver, device, ib, response)
device:emit_event_for_endpoint(ib.endpoint_id, attribute(fields.level_strings[ib.data.value]))
end
end

local function unit_conversion(device, value, from_unit, to_unit)
local conversion_function = fields.conversion_tables[from_unit][to_unit]
if conversion_function == nil then
device.log.info_with( {hub_logs = true} , string.format("Unsupported unit conversion from %s to %s", fields.unit_strings[from_unit], fields.unit_strings[to_unit]))
return 1
end

if value == nil then
device.log.info_with( {hub_logs = true} , "unit conversion value is nil")
return 1
end
return conversion_function(value)
end

function AirQualityServerAttributeHandlers.measured_value_factory(capability_name, attribute, target_unit)
return function(driver, device, ib, response)
local reporting_unit = device:get_field(capability_name.."_unit")

if reporting_unit == nil then
reporting_unit = fields.unit_default[capability_name]
device:set_field(capability_name.."_unit", reporting_unit, {persist = true})
end

if reporting_unit then
local value = unit_conversion(device, ib.data.value, reporting_unit, target_unit)
device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = value, unit = fields.unit_strings[target_unit]}))

-- handle case where device profile supports both fineDustLevel and dustLevel
if capability_name == capabilities.fineDustSensor.NAME and device:supports_capability(capabilities.dustSensor) then
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.dustSensor.fineDustLevel({value = value, unit = fields.unit_strings[target_unit]}))
end
end
end
end


-- [[ AIR QUALITY CLUSTER ATTRIBUTES ]] --

function AirQualityServerAttributeHandlers.air_quality_handler(driver, device, ib, response)
local state = ib.data.value
if state == 0 then -- Unknown
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unknown())
elseif state == 1 then -- Good
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.good())
elseif state == 2 then -- Fair
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.moderate())
elseif state == 3 then -- Moderate
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.slightlyUnhealthy())
elseif state == 4 then -- Poor
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unhealthy())
elseif state == 5 then -- VeryPoor
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.veryUnhealthy())
elseif state == 6 then -- ExtremelyPoor
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.hazardous())
end
end


-- [[ PRESSURE MEASUREMENT CLUSTER ATTRIBUTES ]] --

function AirQualityServerAttributeHandlers.pressure_measured_value_handler(driver, device, ib, response)
local pressure = st_utils.round(ib.data.value / 10.0)
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.atmosphericPressureMeasurement.atmosphericPressure(pressure))
end

return AirQualityServerAttributeHandlers
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
-- Copyright © 2025 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local version = require "version"
local capabilities = require "st.capabilities"
local clusters = require "st.matter.clusters"
local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils"
local fields = require "sub_drivers.air_quality_sensor.fields"
local version = require "version"
local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields"

local DeviceConfiguration = {}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
-- Copyright © 2025 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local capabilities = require "st.capabilities"
local clusters = require "st.matter.clusters"
local utils = require "st.utils"
local version = require "version"
local utils = require "st.utils"
local clusters = require "st.matter.clusters"
local capabilities = require "st.capabilities"

-- Include driver-side definitions when lua libs api version is < 10
if version.api < 10 then
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,13 @@
-- Copyright © 2025 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local capabilities = require "st.capabilities"
local clusters = require "st.matter.clusters"
local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils"
local fields = require "sub_drivers.air_quality_sensor.fields"
local sensor_utils = require "sensor_utils.utils"
local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils"
local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields"

local LegacyDeviceConfiguration = {}

local function set_supported_health_concern_values(device, setter_function, cluster, cluster_ep)
-- read_datatype_value works since all the healthConcern capabilities' datatypes are equivalent to the one in airQualityHealthConcern
local read_datatype_value = capabilities.airQualityHealthConcern.airQualityHealthConcern
local supported_values = {read_datatype_value.unknown.NAME, read_datatype_value.good.NAME, read_datatype_value.unhealthy.NAME}
if cluster == clusters.AirQuality then
if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.FAIR }) > 0 then
table.insert(supported_values, 3, read_datatype_value.moderate.NAME)
end
if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.MODERATE }) > 0 then
table.insert(supported_values, 4, read_datatype_value.slightlyUnhealthy.NAME)
end
if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.VERY_POOR }) > 0 then
table.insert(supported_values, read_datatype_value.veryUnhealthy.NAME)
end
if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.EXTREMELY_POOR }) > 0 then
table.insert(supported_values, read_datatype_value.hazardous.NAME)
end
else -- ConcentrationMeasurement clusters
if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.MEDIUM_LEVEL }) > 0 then
table.insert(supported_values, 3, read_datatype_value.moderate.NAME)
end
if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.CRITICAL_LEVEL }) > 0 then
table.insert(supported_values, read_datatype_value.hazardous.NAME)
end
end
device:emit_event_for_endpoint(cluster_ep, setter_function(supported_values, { visibility = { displayed = false }}))
end

function LegacyDeviceConfiguration.create_level_measurement_profile(device)
local meas_name, level_name = "", ""
for _, cap in ipairs(fields.CONCENTRATION_MEASUREMENT_PROFILE_ORDERING) do
Expand All @@ -47,7 +18,6 @@ function LegacyDeviceConfiguration.create_level_measurement_profile(device)
local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION })
if #attr_eps > 0 then
level_name = level_name .. fields.CONCENTRATION_MEASUREMENT_MAP[cap][1]
set_supported_health_concern_values(device, fields.CONCENTRATION_MEASUREMENT_MAP[cap][3], cluster, attr_eps[1])
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 })
Expand All @@ -65,8 +35,6 @@ function LegacyDeviceConfiguration.match_profile(device)
local humidity_eps = embedded_cluster_utils.get_endpoints(device, clusters.RelativeHumidityMeasurement.ID)

local profile_name = "aqs"
local aq_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID)
set_supported_health_concern_values(device, capabilities.airQualityHealthConcern.supportedAirQualityValues, clusters.AirQuality, aq_eps[1])

if #temp_eps > 0 then
profile_name = profile_name .. "-temp"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
-- Copyright © 2025 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local capabilities = require "st.capabilities"
local clusters = require "st.matter.clusters"
local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils"
local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields"


local AirQualitySensorUtils = {}

function AirQualitySensorUtils.is_matter_air_quality_sensor(opts, driver, device)
for _, ep in ipairs(device.endpoints) do
for _, dt in ipairs(ep.device_types) do
if dt.device_type_id == fields.AIR_QUALITY_SENSOR_DEVICE_TYPE_ID then
return true
end
end
end

return false
end

function AirQualitySensorUtils.supports_capability_by_id_modular(device, capability, component)
if not device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then
device.log.warn_with({hub_logs = true}, "Device has overriden supports_capability_by_id, but does not have supported capabilities set.")
return false
end
for _, component_capabilities in ipairs(device:get_field(fields.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 get_supported_health_concern_values_for_air_quality(device)
local health_concern_datatype = capabilities.airQualityHealthConcern.airQualityHealthConcern
local supported_values = {health_concern_datatype.unknown.NAME, health_concern_datatype.good.NAME, health_concern_datatype.unhealthy.NAME}
if #embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID, { feature_bitmap = clusters.AirQuality.types.Feature.FAIR }) > 0 then
table.insert(supported_values, 3, health_concern_datatype.moderate.NAME)
end
if #embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID, { feature_bitmap = clusters.AirQuality.types.Feature.MODERATE }) > 0 then
table.insert(supported_values, 4, health_concern_datatype.slightlyUnhealthy.NAME)
end
if #embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID, { feature_bitmap = clusters.AirQuality.types.Feature.VERY_POOR }) > 0 then
table.insert(supported_values, health_concern_datatype.veryUnhealthy.NAME)
end
if #embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID, { feature_bitmap = clusters.AirQuality.types.Feature.EXTREMELY_POOR }) > 0 then
table.insert(supported_values, health_concern_datatype.hazardous.NAME)
end
return supported_values
end

local function get_supported_health_concern_values_for_concentration_cluster(device, cluster)
-- note: health_concern_datatype is generic since all the healthConcern capabilities' datatypes are equivalent to those in airQualityHealthConcern
local health_concern_datatype = capabilities.airQualityHealthConcern.airQualityHealthConcern
local supported_values = {health_concern_datatype.unknown.NAME, health_concern_datatype.good.NAME, health_concern_datatype.unhealthy.NAME}
if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.MEDIUM_LEVEL }) > 0 then
table.insert(supported_values, 3, health_concern_datatype.moderate.NAME)
end
if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.CRITICAL_LEVEL }) > 0 then
table.insert(supported_values, health_concern_datatype.hazardous.NAME)
end
return supported_values
end

function AirQualitySensorUtils.set_supported_health_concern_values(device)
-- handle AQ Health Concern, since this is a mandatory capability
local supported_aqs_values = get_supported_health_concern_values_for_air_quality(device)
local aqs_ep_ids = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) or {}
device:emit_event_for_endpoint(aqs_ep_ids[1], capabilities.airQualityHealthConcern.supportedAirQualityValues(supported_aqs_values, { visibility = { displayed = false }}))

for _, capability in ipairs(fields.CONCENTRATION_MEASUREMENT_PROFILE_ORDERING) do
-- all of these capabilities are optional, and capabilities stored in this field are for either a HealthConcern or a Measurement/Sensor
if device:supports_capability_by_id(capability.ID) and capability.ID:match("HealthConcern$") then
local cluster_info = fields.CONCENTRATION_MEASUREMENT_MAP[capability][2]
local supported_values_setter = fields.CONCENTRATION_MEASUREMENT_MAP[capability][3]
local supported_values = get_supported_health_concern_values_for_concentration_cluster(device, cluster_info)
local cluster_ep_ids = embedded_cluster_utils.get_endpoints(device, cluster_info.ID, { feature_bitmap = cluster_info.types.Feature.LEVEL_INDICATION }) or {} -- cluster associated with the supported capability
device:emit_event_for_endpoint(cluster_ep_ids[1], supported_values_setter(supported_values, { visibility = { displayed = false }}))
end
end
end

return AirQualitySensorUtils
Loading
Loading