diff --git a/drivers/SmartThings/matter-sensor/profiles/aqs-modular.yml b/drivers/SmartThings/matter-sensor/profiles/aqs-modular.yml new file mode 100644 index 0000000000..f7c7c4f4d8 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/aqs-modular.yml @@ -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 diff --git a/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua b/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua index 23e69c54a9..792e581d55 100644 --- a/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua +++ b/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua @@ -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 @@ -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 @@ -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 @@ -271,6 +288,86 @@ local function do_configure(driver, device) device:try_update_metadata({profile = profile_name}) end +local function supports_capability_by_id_modular(device, capability, component) + 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) + + print(utils.stringify_table({profile = "aqs-modular", optional_component_capabilities = optional_supported_component_capabilities}, nil, true)) + + 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) + + device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + + --re-up subscription with new capabiltiies using the moudlar supports_capability override + device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) + device:subscribe() +end + +local function do_configure(driver, device) +-- 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 + if version.api >= 14 and version.rpc >= 8 then + match_modular_profile(driver, device) + else + match_profile_switch(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})