diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-modular.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-modular.yml index 7cb292c3b1..0ef2706699 100644 --- a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-modular.yml +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-modular.yml @@ -2,6 +2,9 @@ name: air-purifier-modular components: - id: main capabilities: + - id: switch + version: 1 + optional: true - id: airPurifierFanMode version: 1 - id: fanSpeedPercent diff --git a/drivers/SmartThings/matter-thermostat/profiles/fan-modular.yml b/drivers/SmartThings/matter-thermostat/profiles/fan-modular.yml new file mode 100644 index 0000000000..eb5ed0eda2 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/fan-modular.yml @@ -0,0 +1,26 @@ +name: fan-modular +components: + - id: main + capabilities: + - id: switch + version: 1 + optional: true + - id: fanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: fanOscillationMode + version: 1 + optional: true + - id: windMode + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Fan +metadata: + mnmn: SmartThingsEdge + vid: generic-fan-rock-wind diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua index 745076cd04..adfd3c33dc 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua @@ -49,6 +49,7 @@ local mock_device_basic = test.mock_device.build_test_matter_device({ {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 0}, {cluster_id = clusters.HepaFilterMonitoring.ID, cluster_type = "SERVER", feature_map = 7}, {cluster_id = clusters.ActivatedCarbonFilterMonitoring.ID, cluster_type = "SERVER", feature_map = 7}, + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"} }, device_types = { {device_type_id = 0x002D, device_type_revision = 1} -- AP @@ -256,7 +257,7 @@ local function test_init_ap_thermo_aqs_preconfigured() test.socket.matter:__expect_send({ mock_device_ap_thermo_aqs.id, read_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_ap_thermo_aqs.id, "init" }) - local subscribe_request = nil + local subscribe_request for _, attributes in pairs(cluster_subscribe_list_configured) do for _, attribute in ipairs(attributes) do if subscribe_request == nil then @@ -269,11 +270,13 @@ local function test_init_ap_thermo_aqs_preconfigured() test.socket.matter:__expect_send({mock_device_ap_thermo_aqs.id, subscribe_request}) end -local expected_update_metadata= { - optional_component_capabilities={ +local expected_update_metadata = { + optional_component_capabilities = { { "main", - {}, + { + "switch", + }, }, { "activatedCarbonFilter", @@ -293,13 +296,6 @@ local expected_update_metadata= { profile="air-purifier-modular", } -local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_basic) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_basic)) - end - end - test.register_coroutine_test( "Test profile change on init for basic Air Purifier device", function() @@ -313,13 +309,20 @@ test.register_coroutine_test( {enabled_optional_capabilities = expected_update_metadata.optional_component_capabilities} ) test.socket.device_lifecycle:__queue_receive(mock_device_basic:generate_info_changed({ profile = updated_device_profile })) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_basic) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_basic)) + end + end + subscribe_request:merge(clusters.OnOff.attributes.OnOff:subscribe(mock_device_basic)) test.socket.matter:__expect_send({mock_device_basic.id, subscribe_request}) end, { test_init = test_init_basic } ) -local expected_update_metadata= { - optional_component_capabilities={ +local expected_update_metadata_configured = { + optional_component_capabilities = { { "main", { @@ -358,17 +361,6 @@ local expected_update_metadata= { profile="air-purifier-modular", } -local subscribe_request = nil -for _, attributes in pairs(cluster_subscribe_list_configured) do - for _, attribute in ipairs(attributes) do - if subscribe_request == nil then - subscribe_request = attribute:subscribe(mock_device_ap_thermo_aqs) - else - subscribe_request:merge(attribute:subscribe(mock_device_ap_thermo_aqs)) - end - end -end - test.register_coroutine_test( "Test profile change on init for AP and Thermo and AQS combined device type", function() @@ -379,14 +371,24 @@ test.register_coroutine_test( mock_device_ap_thermo_aqs.id, clusters.Thermostat.attributes.AttributeList:build_test_report_data(mock_device_ap_thermo_aqs, 1, {uint32(0)}) }) - mock_device_ap_thermo_aqs:expect_metadata_update(expected_update_metadata) + mock_device_ap_thermo_aqs:expect_metadata_update(expected_update_metadata_configured) test.wait_for_events() local updated_device_profile = t_utils.get_profile_definition("air-purifier-modular.yml", - {enabled_optional_capabilities = expected_update_metadata.optional_component_capabilities} + {enabled_optional_capabilities = expected_update_metadata_configured.optional_component_capabilities} ) test.socket.device_lifecycle:__queue_receive(mock_device_ap_thermo_aqs:generate_info_changed({ profile = updated_device_profile })) + local subscribe_request + for _, attributes in pairs(cluster_subscribe_list_configured) do + for _, attribute in ipairs(attributes) do + if subscribe_request == nil then + subscribe_request = attribute:subscribe(mock_device_ap_thermo_aqs) + else + subscribe_request:merge(attribute:subscribe(mock_device_ap_thermo_aqs)) + end + end + end test.socket.matter:__expect_send({mock_device_ap_thermo_aqs.id, subscribe_request}) end, { test_init = test_init_ap_thermo_aqs_preconfigured } diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua index 8eefceb23b..e2f38d8c0e 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua @@ -2,8 +2,8 @@ -- Licensed under the Apache License, Version 2.0 local test = require "integration_test" +test.set_rpc_version(0) local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan_modular.lua new file mode 100644 index 0000000000..1fe3dddcae --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan_modular.lua @@ -0,0 +1,279 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local im = require "st.matter.interaction_model" +local t_utils = require "integration_test.utils" +local test = require "integration_test" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("fan-modular.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.FanControl.ID, cluster_type = "SERVER", feature_map = 15}, + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"} + }, + device_types = { + {device_type_id = 0x002B, device_type_revision = 1} -- Fan + } + } + } +}) + +local subscribe_request + +local function read_req_on_added(device) + local attributes = { + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.RockSupport, + } + local read_request = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, clus in ipairs(attributes) do + read_request:merge(clus:read(device)) + end + test.socket.matter:__expect_send({ device.id, read_request }) +end + +local function test_init() + test.mock_device.add_test_device(mock_device) + local cluster_subscribe_list = { + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.PercentCurrent, + } + subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + read_req_on_added(mock_device) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) +end +test.set_test_init_function(test_init) + +local cluster_subscribe_list_configured = { + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.WindSetting, + clusters.FanControl.attributes.RockSupport, + clusters.FanControl.attributes.RockSetting, + clusters.OnOff.attributes.OnOff, +} + +local expected_metadata = { + optional_component_capabilities = { { "main", { "switch", "fanOscillationMode", "windMode", } } }, + profile = "fan-modular" +} + +local function update_device_profile() + mock_device:set_field("__BATTERY_SUPPORT", "NO_BATTERY") + mock_device:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update(expected_metadata) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + local updated_device_profile = t_utils.get_profile_definition( + "fan-modular.yml", { enabled_optional_capabilities = expected_metadata.optional_component_capabilities } + ) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_profile })) + for _, attr in ipairs(cluster_subscribe_list_configured) do + subscribe_request:merge(attr:subscribe(mock_device)) + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) +end + +test.register_coroutine_test( + "Test fan speed commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.FanControl.attributes.PercentCurrent:build_test_report_data(mock_device, 1, 10) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.fanSpeedPercent.percent(10)) + ) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "fanSpeedPercent", component = "main", command = "setPercent", args = { 50 } } + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + clusters.FanControl.attributes.PercentSetting:write(mock_device, 1, 50) + } + ) + end +) + +local supportedFanWind = { + capabilities.windMode.windMode.noWind.NAME, + capabilities.windMode.windMode.sleepWind.NAME, + capabilities.windMode.windMode.naturalWind.NAME +} + +test.register_coroutine_test( + "Test wind mode", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.FanControl.attributes.WindSupport:build_test_report_data(mock_device, 1, 0x03) -- NoWind, SleepWind (0x0001), and NaturalWind (0x0002) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.windMode.supportedWindModes(supportedFanWind, { visibility = { displayed = false } }) + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.FanControl.attributes.WindSetting:build_test_report_data( + mock_device, + 1, + clusters.FanControl.types.WindSettingMask.SLEEP_WIND + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windMode.windMode.sleepWind()) + ) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windMode", component = "main", command = "setWindMode", args = { "naturalWind" } } + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + clusters.FanControl.attributes.WindSetting:write( + mock_device, + 1, + clusters.FanControl.types.WindSettingMask.NATURAL_WIND + ) + } + ) + end +) + +test.register_coroutine_test( + "Test fan mode handler", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.FanControl.attributes.FanMode:build_test_report_data( + mock_device, + 1, + clusters.FanControl.attributes.FanMode.OFF + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.fanMode.fanMode("off")) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.FanControl.attributes.FanMode:build_test_report_data( + mock_device, + 1, + clusters.FanControl.attributes.FanMode.LOW + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.fanMode.fanMode("low")) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.FanControl.attributes.FanMode:build_test_report_data( + mock_device, + 1, + clusters.FanControl.attributes.FanMode.HIGH + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.fanMode.fanMode("high")) + ) + end +) + +test.register_coroutine_test( + "Fan Mode sequence reports should generate the appropriate supported modes", + function() + update_device_profile() + test.wait_for_events() + local FanModeSequence = clusters.FanControl.attributes.FanModeSequence + test.socket.matter:__queue_receive( + { + mock_device.id, + FanModeSequence:build_test_report_data(mock_device, 1, FanModeSequence.OFF_ON) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.fanMode.supportedFanModes( + { "off", "high" }, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + FanModeSequence:build_test_report_data(mock_device, 1, FanModeSequence.OFF_LOW_MED_HIGH_AUTO) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.fanMode.supportedFanModes( + { "off", "low", "medium", "high", "auto" }, + { visibility = { displayed = false } } + ) + ) + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua index 3ae5c76c7e..fb96cfae4f 100644 --- a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua @@ -84,7 +84,7 @@ end local DeviceConfiguration = {} -function DeviceConfiguration.match_modular_profile_air_purifer(device) +function DeviceConfiguration.match_modular_profile_air_purifier(device) local optional_supported_component_capabilities = {} local main_component_capabilities = {} local hepa_filter_component_capabilities = {} @@ -94,6 +94,11 @@ function DeviceConfiguration.match_modular_profile_air_purifer(device) local MAIN_COMPONENT_IDX = 1 local CAPABILITIES_LIST_IDX = 2 + local on_off_eps = device:get_endpoints(clusters.OnOff.ID) + if #on_off_eps > 0 then + table.insert(main_component_capabilities, capabilities.switch.ID) + end + local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) if #humidity_eps > 0 then @@ -184,31 +189,22 @@ function DeviceConfiguration.match_modular_profile_air_purifer(device) end end -function DeviceConfiguration.match_modular_profile_thermostat(device) +function DeviceConfiguration.match_modular_profile_fan(device) local optional_supported_component_capabilities = {} local main_component_capabilities = {} - local profile_name = "thermostat-modular" + local profile_name = "fan-modular" - local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) - if #humidity_eps > 0 then - table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) + local MAIN_COMPONENT_IDX = 1 + local CAPABILITIES_LIST_IDX = 2 + + local on_off_eps = device:get_endpoints(clusters.OnOff.ID) + if #on_off_eps > 0 then + table.insert(main_component_capabilities, capabilities.switch.ID) end - -- determine fan capabilities - local fan_eps = device:get_endpoints(clusters.FanControl.ID) local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) - if #fan_eps > 0 then - if #device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.MULTI_SPEED}) > 0 then - table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID) - if #device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.AUTO}) > 0 then - table.insert(main_component_capabilities, capabilities.fanMode.ID) - end - else - table.insert(main_component_capabilities, capabilities.fanMode.ID) - end - end if #rock_eps > 0 then table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) end @@ -216,19 +212,8 @@ function DeviceConfiguration.match_modular_profile_thermostat(device) table.insert(main_component_capabilities, capabilities.windMode.ID) end - local thermostat_capabilities = DeviceConfigurationHelpers.get_thermostat_optional_capabilities(device) - for _, capability_id in pairs(thermostat_capabilities) do - table.insert(main_component_capabilities, capability_id) - end - - local battery_supported = device:get_field(fields.profiling_data.BATTERY_SUPPORT) - if battery_supported == fields.battery_support.BATTERY_LEVEL then - table.insert(main_component_capabilities, capabilities.batteryLevel.ID) - elseif battery_supported == fields.battery_support.BATTERY_PERCENTAGE then - table.insert(main_component_capabilities, capabilities.battery.ID) - end - table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) -- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW. @@ -236,10 +221,10 @@ function DeviceConfiguration.match_modular_profile_thermostat(device) if version.api < 15 or version.rpc < 9 then -- add mandatory capabilities for subscription local total_supported_capabilities = optional_supported_component_capabilities - table.insert(main_component_capabilities, capabilities.thermostatMode.ID) - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - table.insert(main_component_capabilities, capabilities.refresh.ID) - table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.fanMode.ID) + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.fanSpeedPercent.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(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) end @@ -301,8 +286,70 @@ function DeviceConfiguration.match_modular_profile_room_ac(device) end end +function DeviceConfiguration.match_modular_profile_thermostat(device) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + local profile_name = "thermostat-modular" + + local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) + if #humidity_eps > 0 then + table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) + end + + -- determine fan capabilities + local fan_eps = device:get_endpoints(clusters.FanControl.ID) + local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) + local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) + + if #fan_eps > 0 then + if #device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.MULTI_SPEED}) > 0 then + table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID) + if #device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.AUTO}) > 0 then + table.insert(main_component_capabilities, capabilities.fanMode.ID) + end + else + table.insert(main_component_capabilities, capabilities.fanMode.ID) + end + end + if #rock_eps > 0 then + table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) + end + if #wind_eps > 0 then + table.insert(main_component_capabilities, capabilities.windMode.ID) + end + + local thermostat_capabilities = DeviceConfigurationHelpers.get_thermostat_optional_capabilities(device) + for _, capability_id in pairs(thermostat_capabilities) do + table.insert(main_component_capabilities, capability_id) + end + + local battery_supported = device:get_field(fields.profiling_data.BATTERY_SUPPORT) + if battery_supported == fields.battery_support.BATTERY_LEVEL then + table.insert(main_component_capabilities, capabilities.batteryLevel.ID) + elseif battery_supported == fields.battery_support.BATTERY_PERCENTAGE then + table.insert(main_component_capabilities, capabilities.battery.ID) + end + + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) + + -- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW. + -- This gating specifies a workaround required only for 0.57 FW, which is not needed for 0.58 and higher. + if version.api < 15 or version.rpc < 9 then + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + table.insert(main_component_capabilities, capabilities.thermostatMode.ID) + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + table.insert(main_component_capabilities, capabilities.refresh.ID) + table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) + + device:set_field(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end +end + local match_modular_device_type = { - [fields.AP_DEVICE_TYPE_ID] = DeviceConfiguration.match_modular_profile_air_purifer, + [fields.AP_DEVICE_TYPE_ID] = DeviceConfiguration.match_modular_profile_air_purifier, + [fields.FAN_DEVICE_TYPE_ID] = DeviceConfiguration.match_modular_profile_fan, [fields.RAC_DEVICE_TYPE_ID] = DeviceConfiguration.match_modular_profile_room_ac, [fields.THERMOSTAT_DEVICE_TYPE_ID] = DeviceConfiguration.match_modular_profile_thermostat, }