Repository: SmartThingsCommunity/SmartThingsEdgeDrivers
Pull request #2082: Matter Sensor: Add modular profile supports for AQS
Author: login: ctowns
Created: 2025-04-18T17:14:10Z | Merged: 2025-05-23T16:46:05Z

Check all that apply

# Type of Change

- [ ] WWST Certification Request
     - If this is your first time contributing code:
          - [ ] I have reviewed the README.md file
          - [ ] I have reviewed the CODE_OF_CONDUCT.md file
          - [ ] I have signed the CLA
     - [ ] I plan on entering a WWST Certification Request or have entered a request through the WWST Certification console at developer.smartthings.com
- [ ] Bug fix
- [x] New feature
- [ ] Refactor

# Checklist

- [x] I have performed a self-review of my code
- [x] I have commented my code in hard-to-understand areas
- [x] I have verified my changes by testing with a device or have communicated a plan for testing
- [x] I am adding new behavior, such as adding a sub-driver, and have added and run new unit tests to cover the new behavior

# Description of Change


# Summary of Completed Tests
Added additional unit tests for common AQS configuration.
Onboarded AQS VDA device and confirmed functionality of all available attributes and capabilities.
Completed test plan as defined in REQ





Diff:
diff --git a/drivers/SmartThings/matter-sensor/profiles/aqs-modular-humidity.yml b/drivers/SmartThings/matter-sensor/profiles/aqs-modular-humidity.yml
new file mode 100644
index 0000000000..ad34c342dd
--- /dev/null
+++ b/drivers/SmartThings/matter-sensor/profiles/aqs-modular-humidity.yml
@@ -0,0 +1,81 @@
+name: aqs-modular-humidity
+components:
+  - id: main
+    capabilities:
+      - id: airQualityHealthConcern
+        version: 1
+      - id: temperatureMeasurement
+        version: 1
+        optional: true
+      - id: relativeHumidityMeasurement
+        version: 1
+        optional: true
+      - id: carbonMonoxideMeasurement
+        version: 1
+        optional: true
+      - id: carbonMonoxideHealthConcern
+        version: 1
+        optional: true
+      - id: carbonDioxideMeasurement
+        version: 1
+        optional: true
+      - id: carbonDioxideHealthConcern
+        version: 1
+        optional: true
+      - id: nitrogenDioxideMeasurement
+        version: 1
+        optional: true
+      - id: nitrogenDioxideHealthConcern
+        version: 1
+        optional: true
+      - id: ozoneMeasurement
+        version: 1
+        optional: true
+      - id: ozoneHealthConcern
+        version: 1
+        optional: true
+      - id: formaldehydeMeasurement
+        version: 1
+        optional: true
+      - id: formaldehydeHealthConcern
+        version: 1
+        optional: true
+      - id: veryFineDustSensor
+        version: 1
+        optional: true
+      - id: veryFineDustHealthConcern
+        version: 1
+        optional: true
+      - id: fineDustSensor
+        version: 1
+        optional: true
+      - id: fineDustHealthConcern
+        version: 1
+        optional: true
+      - id: dustSensor
+        version: 1
+        optional: true
+      - id: dustHealthConcern
+        version: 1
+        optional: true
+      - id: radonMeasurement
+        version: 1
+        optional: true
+      - id: radonHealthConcern
+        version: 1
+        optional: true
+      - id: tvocMeasurement
+        version: 1
+        optional: true
+      - id: tvocHealthConcern
+        version: 1
+        optional: true
+      - id: firmwareUpdate
+        version: 1
+      - id: refresh
+        version: 1
+    categories:
+    - name: AirQualityDetector
+preferences:
+  - preferenceId: humidityOffset
+    explicit: true
diff --git a/drivers/SmartThings/matter-sensor/profiles/aqs-modular-temp-humidity.yml b/drivers/SmartThings/matter-sensor/profiles/aqs-modular-temp-humidity.yml
new file mode 100644
index 0000000000..1f116c5a26
--- /dev/null
+++ b/drivers/SmartThings/matter-sensor/profiles/aqs-modular-temp-humidity.yml
@@ -0,0 +1,83 @@
+name: aqs-modular-temp-humidity
+components:
+  - id: main
+    capabilities:
+      - id: airQualityHealthConcern
+        version: 1
+      - id: temperatureMeasurement
+        version: 1
+        optional: true
+      - id: relativeHumidityMeasurement
+        version: 1
+        optional: true
+      - id: carbonMonoxideMeasurement
+        version: 1
+        optional: true
+      - id: carbonMonoxideHealthConcern
+        version: 1
+        optional: true
+      - id: carbonDioxideMeasurement
+        version: 1
+        optional: true
+      - id: carbonDioxideHealthConcern
+        version: 1
+        optional: true
+      - id: nitrogenDioxideMeasurement
+        version: 1
+        optional: true
+      - id: nitrogenDioxideHealthConcern
+        version: 1
+        optional: true
+      - id: ozoneMeasurement
+        version: 1
+        optional: true
+      - id: ozoneHealthConcern
+        version: 1
+        optional: true
+      - id: formaldehydeMeasurement
+        version: 1
+        optional: true
+      - id: formaldehydeHealthConcern
+        version: 1
+        optional: true
+      - id: veryFineDustSensor
+        version: 1
+        optional: true
+      - id: veryFineDustHealthConcern
+        version: 1
+        optional: true
+      - id: fineDustSensor
+        version: 1
+        optional: true
+      - id: fineDustHealthConcern
+        version: 1
+        optional: true
+      - id: dustSensor
+        version: 1
+        optional: true
+      - id: dustHealthConcern
+        version: 1
+        optional: true
+      - id: radonMeasurement
+        version: 1
+        optional: true
+      - id: radonHealthConcern
+        version: 1
+        optional: true
+      - id: tvocMeasurement
+        version: 1
+        optional: true
+      - id: tvocHealthConcern
+        version: 1
+        optional: true
+      - id: firmwareUpdate
+        version: 1
+      - id: refresh
+        version: 1
+    categories:
+    - name: AirQualityDetector
+preferences:
+  - preferenceId: tempOffset
+    explicit: true
+  - preferenceId: humidityOffset
+    explicit: true
diff --git a/drivers/SmartThings/matter-sensor/profiles/aqs-modular-temp.yml b/drivers/SmartThings/matter-sensor/profiles/aqs-modular-temp.yml
new file mode 100644
index 0000000000..b8fd10e3f1
--- /dev/null
+++ b/drivers/SmartThings/matter-sensor/profiles/aqs-modular-temp.yml
@@ -0,0 +1,81 @@
+name: aqs-modular-temp
+components:
+  - id: main
+    capabilities:
+      - id: airQualityHealthConcern
+        version: 1
+      - id: temperatureMeasurement
+        version: 1
+        optional: true
+      - id: relativeHumidityMeasurement
+        version: 1
+        optional: true
+      - id: carbonMonoxideMeasurement
+        version: 1
+        optional: true
+      - id: carbonMonoxideHealthConcern
+        version: 1
+        optional: true
+      - id: carbonDioxideMeasurement
+        version: 1
+        optional: true
+      - id: carbonDioxideHealthConcern
+        version: 1
+        optional: true
+      - id: nitrogenDioxideMeasurement
+        version: 1
+        optional: true
+      - id: nitrogenDioxideHealthConcern
+        version: 1
+        optional: true
+      - id: ozoneMeasurement
+        version: 1
+        optional: true
+      - id: ozoneHealthConcern
+        version: 1
+        optional: true
+      - id: formaldehydeMeasurement
+        version: 1
+        optional: true
+      - id: formaldehydeHealthConcern
+        version: 1
+        optional: true
+      - id: veryFineDustSensor
+        version: 1
+        optional: true
+      - id: veryFineDustHealthConcern
+        version: 1
+        optional: true
+      - id: fineDustSensor
+        version: 1
+        optional: true
+      - id: fineDustHealthConcern
+        version: 1
+        optional: true
+      - id: dustSensor
+        version: 1
+        optional: true
+      - id: dustHealthConcern
+        version: 1
+        optional: true
+      - id: radonMeasurement
+        version: 1
+        optional: true
+      - id: radonHealthConcern
+        version: 1
+        optional: true
+      - id: tvocMeasurement
+        version: 1
+        optional: true
+      - id: tvocHealthConcern
+        version: 1
+        optional: true
+      - id: firmwareUpdate
+        version: 1
+      - id: refresh
+        version: 1
+    categories:
+    - name: AirQualityDetector
+preferences:
+  - preferenceId: humidityOffset
+    explicit: true
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..44466fe9ff
--- /dev/null
+++ b/drivers/SmartThings/matter-sensor/profiles/aqs-modular.yml
@@ -0,0 +1,78 @@
+name: aqs-modular
+components:
+  - id: main
+    capabilities:
+      - id: airQualityHealthConcern
+        version: 1
+      - id: temperatureMeasurement
+        version: 1
+        optional: true
+      - id: relativeHumidityMeasurement
+        version: 1
+        optional: true
+      - id: carbonMonoxideMeasurement
+        version: 1
+        optional: true
+      - id: carbonMonoxideHealthConcern
+        version: 1
+        optional: true
+      - id: carbonDioxideMeasurement
+        version: 1
+        optional: true
+      - id: carbonDioxideHealthConcern
+        version: 1
+        optional: true
+      - id: nitrogenDioxideMeasurement
+        version: 1
+        optional: true
+      - id: nitrogenDioxideHealthConcern
+        version: 1
+        optional: true
+      - id: ozoneMeasurement
+        version: 1
+        optional: true
+      - id: ozoneHealthConcern
+        version: 1
+        optional: true
+      - id: formaldehydeMeasurement
+        version: 1
+        optional: true
+      - id: formaldehydeHealthConcern
+        version: 1
+        optional: true
+      - id: veryFineDustSensor
+        version: 1
+        optional: true
+      - id: veryFineDustHealthConcern
+        version: 1
+        optional: true
+      - id: fineDustSensor
+        version: 1
+        optional: true
+      - id: fineDustHealthConcern
+        version: 1
+        optional: true
+      - id: dustSensor
+        version: 1
+        optional: true
+      - id: dustHealthConcern
+        version: 1
+        optional: true
+      - id: radonMeasurement
+        version: 1
+        optional: true
+      - id: radonHealthConcern
+        version: 1
+        optional: true
+      - id: tvocMeasurement
+        version: 1
+        optional: true
+      - id: tvocHealthConcern
+        version: 1
+        optional: true
+      - id: firmwareUpdate
+        version: 1
+      - id: refresh
+        version: 1
+    categories:
+    - name: AirQualityDetector
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..70b2357e58 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,31 @@ 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
+        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
+        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 +286,108 @@ local function do_configure(driver, device)
   device:try_update_metadata({profile = profile_name})
 end
 
+local function supports_capability_by_id_modular(device, capability, component)
+  if not device:get_field(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(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 = {}
+  local profile_name
+  local MAIN_COMPONENT_IDX = 1
+  local CAPABILITIES_LIST_IDX = 2
+
+  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})
+
+  if #temp_eps > 0 and #humidity_eps > 0 then
+    profile_name = "aqs-modular-temp-humidity"
+  elseif #temp_eps > 0 then
+    profile_name = "aqs-modular-temp"
+  elseif #humidity_eps > 0 then
+    profile_name = "aqs-modular-humidity"
+  else
+    profile_name = "aqs-modular"
+  end
+
+  device:try_update_metadata({profile = profile_name, 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[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airQualityHealthConcern.ID)
+  table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID)
+  table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID)
+
+  device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true })
+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 driver_switched(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})
@@ -416,11 +533,23 @@ local function pressure_attr_handler(driver, device, ib, response)
   device:emit_event_for_endpoint(ib.endpoint_id, capabilities.atmosphericPressureMeasurement.atmosphericPressure(pressure))
 end
 
+local function info_changed(driver, device, event, args)
+  if device.profile.id ~= args.old_st_store.profile.id then
+    if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then
+      --re-up subscription with new capabilities using the modular supports_capability override
+       device:extend_device("supports_capability_by_id", supports_capability_by_id_modular)
+    end
+    device:subscribe()
+  end
+end
+
 local matter_air_quality_sensor_handler = {
   NAME = "matter-air-quality-sensor",
   lifecycle_handlers = {
     init = device_init,
-    doConfigure = do_configure
+    doConfigure = do_configure,
+    infoChanged = info_changed,
+    driverSwitched = driver_switched
   },
   matter_handlers = {
     attr = {
diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua
new file mode 100644
index 0000000000..d799c31b64
--- /dev/null
+++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua
@@ -0,0 +1,319 @@
+-- Copyright 2023 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 utils = require "st.utils"
+local dkjson = require "dkjson"
+
+local clusters = require "st.matter.clusters"
+
+test.set_rpc_version(8)
+
+local mock_device = test.mock_device.build_test_matter_device({
+  profile = t_utils.get_profile_definition("aqs-temp-humidity-all-level-all-meas.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.AirQuality.ID, cluster_type = "SERVER"},
+        {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 = 3},
+        {cluster_id = clusters.CarbonDioxideConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 3},
+        {cluster_id = clusters.NitrogenDioxideConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 3},
+        {cluster_id = clusters.OzoneConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 3},
+        {cluster_id = clusters.FormaldehydeConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 3},
+        {cluster_id = clusters.Pm1ConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 3},
+        {cluster_id = clusters.Pm25ConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 3},
+        {cluster_id = clusters.Pm10ConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 3},
+        {cluster_id = clusters.RadonConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 3},
+        {cluster_id = clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 3},
+      },
+      device_types = {
+        {device_type_id = 0x002C, device_type_revision = 1} -- Air Quality Sensor
+      }
+    }
+  }
+})
+
+local mock_device_common = test.mock_device.build_test_matter_device({
+  profile = t_utils.get_profile_definition("aqs-temp-humidity-co2-pm25-tvoc-meas.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.AirQuality.ID, cluster_type = "SERVER"},
+        {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"},
+        {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"},
+        {cluster_id = clusters.CarbonDioxideConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 1},
+        {cluster_id = clusters.Pm25ConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 1},
+        {cluster_id = clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 1},
+      },
+      device_types = {
+        {device_type_id = 0x002C, device_type_revision = 1} -- Air Quality Sensor
+      }
+    }
+  }
+})
+
+-- create test_init functions
+local function initialize_mock_device(generic_mock_device, generic_subscribed_attributes)
+  local subscribe_request = nil
+  for _, attributes in pairs(generic_subscribed_attributes) do
+    for _, attribute in ipairs(attributes) do
+      if subscribe_request == nil then
+        subscribe_request = attribute:subscribe(generic_mock_device)
+      else
+        subscribe_request:merge(attribute:subscribe(generic_mock_device))
+      end
+    end
+  end
+  test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request})
+  test.mock_device.add_test_device(generic_mock_device)
+  return subscribe_request
+end
+
+local subscribe_request_all
+local function test_init()
+  local subscribed_attributes = {
+    [capabilities.relativeHumidityMeasurement.ID] = {
+      clusters.RelativeHumidityMeasurement.attributes.MeasuredValue
+    },
+    [capabilities.temperatureMeasurement.ID] = {
+      clusters.TemperatureMeasurement.attributes.MeasuredValue,
+      clusters.TemperatureMeasurement.attributes.MinMeasuredValue,
+      clusters.TemperatureMeasurement.attributes.MaxMeasuredValue
+    },
+    [capabilities.airQualityHealthConcern.ID] = {
+      clusters.AirQuality.attributes.AirQuality
+    },
+    [capabilities.carbonMonoxideMeasurement.ID] = {
+      clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue,
+      clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit,
+    },
+    [capabilities.carbonMonoxideHealthConcern.ID] = {
+      clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue,
+    },
+    [capabilities.carbonDioxideMeasurement.ID] = {
+      clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue,
+      clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit,
+    },
+    [capabilities.carbonDioxideHealthConcern.ID] = {
+      clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue,
+    },
+    [capabilities.nitrogenDioxideMeasurement.ID] = {
+      clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue,
+      clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit
+    },
+    [capabilities.nitrogenDioxideHealthConcern.ID] = {
+      clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue,
+    },
+    [capabilities.ozoneMeasurement.ID] = {
+      clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue,
+      clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit
+    },
+    [capabilities.ozoneHealthConcern.ID] = {
+      clusters.OzoneConcentrationMeasurement.attributes.LevelValue,
+    },
+    [capabilities.formaldehydeMeasurement.ID] = {
+      clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue,
+      clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit,
+    },
+    [capabilities.formaldehydeHealthConcern.ID] = {
+      clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue,
+    },
+    [capabilities.veryFineDustSensor.ID] = {
+      clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue,
+      clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit,
+    },
+    [capabilities.veryFineDustHealthConcern.ID] = {
+      clusters.Pm1ConcentrationMeasurement.attributes.LevelValue,
+    },
+    [capabilities.fineDustHealthConcern.ID] = {
+      clusters.Pm25ConcentrationMeasurement.attributes.LevelValue,
+    },
+    [capabilities.dustSensor.ID] = {
+      clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue,
+      clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit,
+      clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue,
+      clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit,
+    },
+    [capabilities.dustHealthConcern.ID] = {
+      clusters.Pm10ConcentrationMeasurement.attributes.LevelValue,
+    },
+    [capabilities.radonMeasurement.ID] = {
+      clusters.RadonConcentrationMeasurement.attributes.MeasuredValue,
+      clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit,
+    },
+    [capabilities.radonHealthConcern.ID] = {
+      clusters.RadonConcentrationMeasurement.attributes.LevelValue,
+    },
+    [capabilities.tvocMeasurement.ID] = {
+      clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue,
+      clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit,
+    },
+    [capabilities.tvocHealthConcern.ID] = {
+      clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue,
+    },
+  }
+  subscribe_request_all = initialize_mock_device(mock_device, subscribed_attributes)
+end
+
+local subscribe_request_common
+local function test_init_common()
+  local subscribed_attributes = {
+    [capabilities.relativeHumidityMeasurement.ID] = {
+      clusters.RelativeHumidityMeasurement.attributes.MeasuredValue
+    },
+    [capabilities.temperatureMeasurement.ID] = {
+      clusters.TemperatureMeasurement.attributes.MeasuredValue,
+      clusters.TemperatureMeasurement.attributes.MinMeasuredValue,
+      clusters.TemperatureMeasurement.attributes.MaxMeasuredValue
+    },
+    [capabilities.airQualityHealthConcern.ID] = {
+      clusters.AirQuality.attributes.AirQuality
+    },
+    [capabilities.carbonDioxideMeasurement.ID] = {
+      clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue,
+      clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit,
+    },
+    [capabilities.dustSensor.ID] = {
+      clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue,
+      clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit,
+    },
+    [capabilities.tvocMeasurement.ID] = {
+      clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue,
+      clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit,
+    },
+  }
+  subscribe_request_common = initialize_mock_device(mock_device_common, subscribed_attributes)
+end
+test.set_test_init_function(test_init)
+
+-- run the profile configuration tests
+local function test_aqs_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request)
+  test.socket.device_lifecycle:__queue_receive({generic_mock_device.id, "doConfigure"})
+  test.socket.matter:__expect_send({generic_mock_device.id, clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit:read()})
+  test.socket.matter:__expect_send({generic_mock_device.id, clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit:read()})
+  test.socket.matter:__expect_send({generic_mock_device.id, clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit:read()})
+  test.socket.matter:__expect_send({generic_mock_device.id, clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit:read()})
+  test.socket.matter:__expect_send({generic_mock_device.id, clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit:read()})
+  test.socket.matter:__expect_send({generic_mock_device.id, clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit:read()})
+  test.socket.matter:__expect_send({generic_mock_device.id, clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit:read()})
+  test.socket.matter:__expect_send({generic_mock_device.id, clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit:read()})
+  test.socket.matter:__expect_send({generic_mock_device.id, clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit:read()})
+  test.socket.matter:__expect_send({generic_mock_device.id, clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit:read()})
+  generic_mock_device:expect_metadata_update(expected_metadata)
+  generic_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
+  local device_info_copy = utils.deep_copy(generic_mock_device.raw_st_data)
+  device_info_copy.profile.id = "aqs-modular"
+  local device_info_json = dkjson.encode(device_info_copy)
+  test.socket.device_lifecycle:__queue_receive({ generic_mock_device.id, "infoChanged", device_info_json })
+  test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request})
+end
+
+local expected_metadata_all = {
+  optional_component_capabilities={
+    {
+      "main",
+      {
+        "temperatureMeasurement",
+        "relativeHumidityMeasurement",
+        "carbonMonoxideMeasurement",
+        "carbonDioxideMeasurement",
+        "nitrogenDioxideMeasurement",
+        "ozoneMeasurement",
+        "formaldehydeMeasurement",
+        "veryFineDustSensor",
+        "fineDustSensor",
+        "dustSensor",
+        "radonMeasurement",
+        "tvocMeasurement",
+        "carbonMonoxideHealthConcern",
+        "carbonDioxideHealthConcern",
+        "nitrogenDioxideHealthConcern",
+        "ozoneHealthConcern",
+        "formaldehydeHealthConcern",
+        "veryFineDustHealthConcern",
+        "fineDustHealthConcern",
+        "dustHealthConcern",
+        "radonHealthConcern",
+        "tvocHealthConcern",
+      },
+    },
+  },
+  profile="aqs-modular-temp-humidity",
+}
+
+test.register_coroutine_test(
+  "Device with modular profile should enable correct optional capabilities - all clusters",
+  function()
+    test_aqs_device_type_update_modular_profile(mock_device, expected_metadata_all, subscribe_request_all)
+  end
+)
+
+local expected_metadata_common = {
+  optional_component_capabilities={
+    {
+      "main",
+      {
+        "temperatureMeasurement",
+        "relativeHumidityMeasurement",
+        "carbonDioxideMeasurement",
+        "fineDustSensor",
+        "tvocMeasurement",
+      },
+    },
+  },
+  profile="aqs-modular-temp-humidity",
+}
+
+test.register_coroutine_test(
+  "Device with modular profile should enable correct optional capabilities - common clusters",
+  function()
+    test_aqs_device_type_update_modular_profile(mock_device_common, expected_metadata_common, subscribe_request_common)
+  end,
+  { test_init = test_init_common }
+)
+
+-- run tests
+test.run_registered_tests()


Reviews:
user: login: hcarter-775
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T17:41:37Z
user: login: hcarter-775
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T17:49:03Z
user: login: hcarter-775
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T18:08:32Z
user: login: hcarter-775
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T18:12:04Z
user: login: nickolas-deboom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T19:33:15Z
user: login: ctowns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T19:53:47Z
user: login: ctowns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T19:54:09Z
user: login: ctowns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T19:54:24Z
user: login: ctowns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T19:54:46Z
user: login: ctowns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T19:55:06Z
user: login: nickolas-deboom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T18:26:49Z
user: login: hcarter-775
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T02:36:26Z
user: login: hcarter-775
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T02:42:21Z
user: login: hcarter-775
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T02:43:58Z
user: login: ctowns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:15:00Z
user: login: ctowns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:20:22Z
user: login: ctowns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:21:01Z
user: login: nickolas-deboom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:28:46Z
user: login: nickolas-deboom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:39:16Z
user: login: nickolas-deboom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:41:07Z
user: login: hcarter-775
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T16:57:44Z
user: login: nickolas-deboom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T18:18:03Z
user: login: nickolas-deboom
body: LGTM, I have tested this with various VDA sensor and took another pass at the changes and things are looking good from my perspective. One thing I forgot to try - did you test the refresh command?
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T19:09:29Z
user: login: hcarter-775
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T19:32:40Z
user: login: ctowns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T19:45:05Z
user: login: ctowns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T19:45:19Z
user: login: ctowns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T19:58:25Z
user: login: hcarter-775
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T22:00:16Z
user: login: hcarter-775
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T22:00:29Z

Review comments:
diff_hunk: @@ -271,6 +293,81 @@ 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)
+
+  -- 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 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)
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: hcarter-775
body: this line does not seem required, since the field is overwritten two lines later
created_at: 2025-04-18T17:41:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +293,81 @@ 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)
+
+  -- 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 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)
+
+  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)
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: hcarter-775
body: nit, though I think inserting into "[1][2]" is a little confusing and asks the reader to trace back what [1][2]  actually references.
created_at: 2025-04-18T17:49:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +293,81 @@ 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)
+
+  -- 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 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)
+
+  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
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: hcarter-775
body: ```suggestion
  --re-up subscription with new capabilities using the modular supports_capability override
```
created_at: 2025-04-18T18:08:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +293,81 @@ local function do_configure(driver, device)
   device:try_update_metadata({profile = profile_name})
 end
 
+local function supports_capability_by_id_modular(device, capability, component)
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: hcarter-775
body: 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? 
created_at: 2025-04-18T18:12:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +293,81 @@ 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)
+
+  -- 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 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)
+
+  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)
+  -- must use profile switching on older hubs
+  if version.api < 14 and version.rpc < 7 then
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: nickolas-deboom
body: Was the RPC version bumped for 0.57?
created_at: 2025-04-18T19:33:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +293,81 @@ 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)
+
+  -- 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 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)
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: ctowns
body: Removed!
created_at: 2025-05-14T19:53:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +293,81 @@ 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)
+
+  -- 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 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)
+
+  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)
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: ctowns
body: Good call out - I added some consts defined at the top of the function to make this more clear
created_at: 2025-05-14T19:54:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +293,81 @@ 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)
+
+  -- 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 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)
+
+  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
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: ctowns
body: Fixed!
created_at: 2025-05-14T19:54:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +293,81 @@ local function do_configure(driver, device)
   device:try_update_metadata({profile = profile_name})
 end
 
+local function supports_capability_by_id_modular(device, capability, component)
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: ctowns
body: I just add this so it was clear why this was being done this way - i.e. only for modular profiles.
created_at: 2025-05-14T19:54:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +293,81 @@ 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)
+
+  -- 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 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)
+
+  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)
+  -- must use profile switching on older hubs
+  if version.api < 14 and version.rpc < 7 then
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: ctowns
body: Yes! RPC version is now 8 and has been updated accordingly!
created_at: 2025-05-14T19:55:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +288,85 @@ 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 = {}
+  local MAIN_COMPONENT_IDX = 1
+  local CAPABILITIES_LIST_IDX = 2
+
+
+  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: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[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airQualityHealthConcern.ID)
+
+  device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true })
+
+  --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)
+-- 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)
+
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: nickolas-deboom
body: nit there's a extra line here
created_at: 2025-05-15T18:26:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +286,82 @@ 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 = {}
+  local MAIN_COMPONENT_IDX = 1
+  local CAPABILITIES_LIST_IDX = 2
+
+
+  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: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[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airQualityHealthConcern.ID)
+  table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID)
+  table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID)
+
+  device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true })
+end
+
+local function do_configure(driver, device)
+-- we have to read the unit before reports of values will do anything
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: hcarter-775
body: ```suggestion
    -- we have to read the unit before reports of values will do anything
```
created_at: 2025-05-16T02:36:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +286,82 @@ 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 = {}
+  local MAIN_COMPONENT_IDX = 1
+  local CAPABILITIES_LIST_IDX = 2
+
+
+  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: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[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airQualityHealthConcern.ID)
+  table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID)
+  table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID)
+
+  device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true })
+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
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: hcarter-775
body: I think this could be a one-time action in `do_configure`. We could probably get away without the extra check as well in `do_configure` since it's required for `SUPPORTED_COMPONENT_CAPABILITIES` to be set in `match_modular_profile` (even if it's set to an empty array):
```
if version.api >= 14 and version.rpc >= 8 then
    match_modular_profile(driver, device)
    device:extend_device("supports_capability_by_id", supports_capability_by_id_modular)
else
```
created_at: 2025-05-16T02:42:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -416,11 +507,22 @@ local function pressure_attr_handler(driver, device, ib, response)
   device:emit_event_for_endpoint(ib.endpoint_id, capabilities.atmosphericPressureMeasurement.atmosphericPressure(pressure))
 end
 
+local function info_changed(driver, device, event, args)
+  if device.profile.id ~= args.old_st_store.profile.id then
+    if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then
+      --re-up subscription with new capabilities using the modular supports_capability override
+       device:extend_device("supports_capability_by_id", supports_capability_by_id_modular)
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: hcarter-775
body: if we move this into `do_configure` like I suggested, it seems superfluous to double handle it in `info_changed`.
created_at: 2025-05-16T02:43:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +286,82 @@ 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 = {}
+  local MAIN_COMPONENT_IDX = 1
+  local CAPABILITIES_LIST_IDX = 2
+
+
+  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: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[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airQualityHealthConcern.ID)
+  table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID)
+  table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID)
+
+  device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true })
+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
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: ctowns
body: the device needs to be extended on each driver init because this override is not persisted
created_at: 2025-05-16T15:15:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +286,82 @@ 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 = {}
+  local MAIN_COMPONENT_IDX = 1
+  local CAPABILITIES_LIST_IDX = 2
+
+
+  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: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[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airQualityHealthConcern.ID)
+  table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID)
+  table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID)
+
+  device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true })
+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
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: ctowns
body: It looks like the fact that this is not persisted is not made explictly clear in our developer docs: https://developer.smartthings.com/docs/edge-device-drivers/device.html#st.Device.extend_device

However, if you look at the definition in our lua libs, you'll see that this override is saved to a table that is not in our persistent store
created_at: 2025-05-16T15:20:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -416,11 +507,22 @@ local function pressure_attr_handler(driver, device, ib, response)
   device:emit_event_for_endpoint(ib.endpoint_id, capabilities.atmosphericPressureMeasurement.atmosphericPressure(pressure))
 end
 
+local function info_changed(driver, device, event, args)
+  if device.profile.id ~= args.old_st_store.profile.id then
+    if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then
+      --re-up subscription with new capabilities using the modular supports_capability override
+       device:extend_device("supports_capability_by_id", supports_capability_by_id_modular)
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: ctowns
body: See comment above, I don't think we can one time configure the library function override ^
created_at: 2025-05-16T15:21:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+name: aqs-modular
+components:
+  - id: main
+    capabilities:
+      - id: airQualityHealthConcern
+        version: 1
+      - id: temperatureMeasurement
+        version: 1
+        optional: true
+      - id: relativeHumidityMeasurement
+        version: 1
+        optional: true
+      - id: carbonMonoxideMeasurement
+        version: 1
+        optional: true
+      - id: carbonMonoxideHealthConcern
+        version: 1
+        optional: true
+      - id: carbonDioxideMeasurement
+        version: 1
+        optional: true
+      - id: carbonDioxideHealthConcern
+        version: 1
+        optional: true
+      - id: nitrogenDioxideMeasurement
+        version: 1
+        optional: true
+      - id: nitrogenDioxideHealthConcern
+        version: 1
+        optional: true
+      - id: ozoneMeasurement
+        version: 1
+        optional: true
+      - id: ozoneHealthConcern
+        version: 1
+        optional: true
+      - id: formaldehydeMeasurement
+        version: 1
+        optional: true
+      - id: formaldehydeHealthConcern
+        version: 1
+        optional: true
+      - id: veryFineDustSensor
+        version: 1
+        optional: true
+      - id: veryFineDustHealthConcern
+        version: 1
+        optional: true
+      - id: fineDustSensor
+        version: 1
+        optional: true
+      - id: fineDustHealthConcern
+        version: 1
+        optional: true
+      - id: dustSensor
+        version: 1
+        optional: true
+      - id: dustHealthConcern
+        version: 1
+        optional: true
+      - id: radonMeasurement
+        version: 1
+        optional: true
+      - id: radonHealthConcern
+        version: 1
+        optional: true
+      - id: tvocMeasurement
+        version: 1
+        optional: true
+      - id: tvocHealthConcern
+        version: 1
+        optional: true
+      - id: firmwareUpdate
+        version: 1
+      - id: refresh
+        version: 1
+    categories:
+    - name: AirQualityDetector
+preferences:
path: drivers/SmartThings/matter-sensor/profiles/aqs-modular.yml
user: login: nickolas-deboom
body: We might need two versions of this profile, one without the preferences, since `temperatureMeasurement` and `relativeHumidityMeasurement` are optional
created_at: 2025-05-16T15:28:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -416,11 +507,22 @@ local function pressure_attr_handler(driver, device, ib, response)
   device:emit_event_for_endpoint(ib.endpoint_id, capabilities.atmosphericPressureMeasurement.atmosphericPressure(pressure))
 end
 
+local function info_changed(driver, device, event, args)
+  if device.profile.id ~= args.old_st_store.profile.id then
+    if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then
+      --re-up subscription with new capabilities using the modular supports_capability override
+       device:extend_device("supports_capability_by_id", supports_capability_by_id_modular)
+    end
+    device:subscribe()
+  end
+end
+
 local matter_air_quality_sensor_handler = {
   NAME = "matter-air-quality-sensor",
   lifecycle_handlers = {
     init = device_init,
-    doConfigure = do_configure
+    doConfigure = do_configure,
+    infoChanged = info_changed,
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: nickolas-deboom
body: Should we implement a `driverSwitched` handler and call `match_modular_profile` from it?
created_at: 2025-05-16T15:39:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,331 @@
+-- Copyright 2023 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 utils = require "st.utils"
+local dkjson = require "dkjson"
+
+local clusters = require "st.matter.clusters"
+
+clusters.AirQuality = require "AirQuality"
path: drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua
user: login: nickolas-deboom
body: Is there a reason you're testing with the embedded clusters rather than from the lua libs?
created_at: 2025-05-16T15:41:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +286,82 @@ 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 = {}
+  local MAIN_COMPONENT_IDX = 1
+  local CAPABILITIES_LIST_IDX = 2
+
+
+  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: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[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airQualityHealthConcern.ID)
+  table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID)
+  table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID)
+
+  device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true })
+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
path: drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua
user: login: hcarter-775
body: yeah I see now, and that makes sense, since otherwise there may not be any way atm to "un-extend" a function after it is extended.
created_at: 2025-05-16T16:57:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+name: aqs-modular-humidity
+components:
+  - id: main
+    capabilities:
+      - id: airQualityHealthConcern
+        version: 1
+      - id: temperatureMeasurement
path: drivers/SmartThings/matter-sensor/profiles/aqs-modular-humidity.yml
user: login: nickolas-deboom
body: Should temp be removed from this profile? Edit: nvm, modular profiles would take care of that :sweat_smile: 
created_at: 2025-05-19T18:18:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,78 @@
+name: aqs-modular
+components:
+  - id: main
+    capabilities:
+      - id: airQualityHealthConcern
+        version: 1
+      - id: temperatureMeasurement
+        version: 1
+        optional: true
+      - id: relativeHumidityMeasurement
+        version: 1
path: drivers/SmartThings/matter-sensor/profiles/aqs-modular.yml
user: login: hcarter-775
body: remove
created_at: 2025-05-19T19:32:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,331 @@
+-- Copyright 2023 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 utils = require "st.utils"
+local dkjson = require "dkjson"
+
+local clusters = require "st.matter.clusters"
+
+clusters.AirQuality = require "AirQuality"
path: drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua
user: login: ctowns
body: No reason! It was just copied over from another test file, but they shouldn't need to be embedded anymore. Removed!
created_at: 2025-05-22T19:45:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+name: aqs-modular
+components:
+  - id: main
+    capabilities:
+      - id: airQualityHealthConcern
+        version: 1
+      - id: temperatureMeasurement
+        version: 1
+        optional: true
+      - id: relativeHumidityMeasurement
+        version: 1
+        optional: true
+      - id: carbonMonoxideMeasurement
+        version: 1
+        optional: true
+      - id: carbonMonoxideHealthConcern
+        version: 1
+        optional: true
+      - id: carbonDioxideMeasurement
+        version: 1
+        optional: true
+      - id: carbonDioxideHealthConcern
+        version: 1
+        optional: true
+      - id: nitrogenDioxideMeasurement
+        version: 1
+        optional: true
+      - id: nitrogenDioxideHealthConcern
+        version: 1
+        optional: true
+      - id: ozoneMeasurement
+        version: 1
+        optional: true
+      - id: ozoneHealthConcern
+        version: 1
+        optional: true
+      - id: formaldehydeMeasurement
+        version: 1
+        optional: true
+      - id: formaldehydeHealthConcern
+        version: 1
+        optional: true
+      - id: veryFineDustSensor
+        version: 1
+        optional: true
+      - id: veryFineDustHealthConcern
+        version: 1
+        optional: true
+      - id: fineDustSensor
+        version: 1
+        optional: true
+      - id: fineDustHealthConcern
+        version: 1
+        optional: true
+      - id: dustSensor
+        version: 1
+        optional: true
+      - id: dustHealthConcern
+        version: 1
+        optional: true
+      - id: radonMeasurement
+        version: 1
+        optional: true
+      - id: radonHealthConcern
+        version: 1
+        optional: true
+      - id: tvocMeasurement
+        version: 1
+        optional: true
+      - id: tvocHealthConcern
+        version: 1
+        optional: true
+      - id: firmwareUpdate
+        version: 1
+      - id: refresh
+        version: 1
+    categories:
+    - name: AirQualityDetector
+preferences:
path: drivers/SmartThings/matter-sensor/profiles/aqs-modular.yml
user: login: ctowns
body: Added these!
created_at: 2025-05-22T19:45:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,78 @@
+name: aqs-modular
+components:
+  - id: main
+    capabilities:
+      - id: airQualityHealthConcern
+        version: 1
+      - id: temperatureMeasurement
+        version: 1
+        optional: true
+      - id: relativeHumidityMeasurement
+        version: 1
path: drivers/SmartThings/matter-sensor/profiles/aqs-modular.yml
user: login: ctowns
body: I'm almost partial to keeping all the capabilities in each of these profiles since these additional profiles are one-off workarounds for R2 while we wait for modular preference support. However, there's nothing stopping each profile from supporting both temp and humidity, the only difference between the profiles is the preferences. Eventually we will collapse everything down to the single `aqs-modular` profile anyway, so it seems to me that that profile should support temp and humidity, even it if doesn't have the preferences. What do you think?
created_at: 2025-05-22T19:58:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,78 @@
+name: aqs-modular
+components:
+  - id: main
+    capabilities:
+      - id: airQualityHealthConcern
+        version: 1
+      - id: temperatureMeasurement
+        version: 1
+        optional: true
+      - id: relativeHumidityMeasurement
+        version: 1
path: drivers/SmartThings/matter-sensor/profiles/aqs-modular.yml
user: login: hcarter-775
body: Doesn't matter to me too much, your call for your PR. To me it seems a little "incomplete" , but I have no hard feelings either way :+1: 
created_at: 2025-05-22T22:00:15Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-18T17:14:35Z
body: Duplicate profile check: Warning - duplicate profiles detected.
aqs-modular-humidity.yml == aqs-modular-temp.yml

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-18T17:15:22Z
body: Channel deleted.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-18T17:15:49Z
body: ## Test Results
   67 files    437 suites   0s ⏱️
2 242 tests 2 242 ✅ 0 💤 0 ❌
3 833 runs  3 833 ✅ 0 💤 0 ❌

Results for commit 577ae27c.

[test-results]:data:application/gzip;base64,H4sIAFGXMGgC/2WMywrDIBQFfyW47kLUm1v7M0F8gDSJxceq9N+rklBDlzOHM2/i/GoTeUwz3iaSis+dBG9oSlTZh70KWrFOuY2MCXbikorWf+7pX+NnccqvF2FjDPEwseytyu+cH3RGL+rX7DwkO49FHbbN5woEEJVlqIGhAaGpFGApSGuRUgQlBVN0ds6QzxeGowoMCgEAAA==

:recycle: This comment has been updated with latest results.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-18T17:15:51Z
body: <strong></strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `87%` | :x: |
| /home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-sensor/src/embedded-cluster-utils.lua | `45%` | :x: |
| /home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-sensor/src/bosch-button-contact/init.lua | `85%` | :x: |
| /home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua | `85%` | :x: |
| /home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua | `91%` | :white_check_mark: |
| /home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-sensor/src/init.lua | `91%` | :white_check_mark: |

_Minimum allowed coverage is `90%`_

<p align="right">Generated by :monkey: cobertura-action against 577ae27c527d54c0945e059ee70075a942a06ffd </p>
author_association: NONE
user: login: ctowns
created_at: 2025-05-22T19:53:34Z
body: > LGTM, I have tested this with various VDA sensor and took another pass at the changes and things are looking good from my perspective. One thing I forgot to try - did you test the refresh command?

Thank Nick! And yeah, just confiming that I did test the `refresh` command it is working as expected, good call out :+1: 
author_association: CONTRIBUTOR
user: login: ctowns
created_at: 2025-05-23T15:40:34Z
body: Squashed and rebased, no merge conflicts.
author_association: CONTRIBUTOR