From 191899faf7e25295afaebeef851a3ed619a0c21c Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:56:05 -0500 Subject: [PATCH] CHAD-13561: Add support for FanControl's Rocking functionality (#1578) Add driver support for the Fan Control cluster's Rocking attributes. --- .../matter-thermostat/fingerprints.yml | 2 +- .../profiles/air-purifier-ac-rock-wind.yml | 28 ++++ .../profiles/air-purifier-ac-rock.yml | 26 ++++ ...2o-meas-pm10-pm25-ch2o-no2-tvoc-level.yml} | 4 +- ...yml => air-purifier-hepa-ac-rock-wind.yml} | 16 +-- .../profiles/air-purifier-hepa-ac-rock.yml | 35 +++++ .../profiles/air-purifier-hepa-rock-wind.yml | 28 ++++ .../profiles/air-purifier-hepa-rock.yml | 26 ++++ .../profiles/air-purifier-rock-wind.yml | 18 +++ .../profiles/air-purifier-rock.yml | 16 +++ .../profiles/fan-generic.yml | 17 +++ .../profiles/fan-rock-wind.yml | 21 +++ .../matter-thermostat/profiles/fan-rock.yml | 19 +++ .../matter-thermostat/profiles/fan-wind.yml | 19 +++ .../matter-thermostat/src/init.lua | 78 ++++++++++- .../src/test/test_matter_air_purifier.lua | 126 ++++++++++++++++- .../src/test/test_matter_fan.lua | 132 ++++++++++++++++++ 17 files changed, 590 insertions(+), 21 deletions(-) create mode 100644 drivers/SmartThings/matter-thermostat/profiles/air-purifier-ac-rock-wind.yml create mode 100644 drivers/SmartThings/matter-thermostat/profiles/air-purifier-ac-rock.yml rename drivers/SmartThings/matter-thermostat/profiles/{air-purifier-hepa-ac-wind-thermostat-humidity-fan-heating-only-nostate-nobattery-aqs-pm10-pm25-ch2o-meas-pm10-pm25-ch2o-no2-tvoc-level.yml => air-purifier-hepa-ac-rock-wind-thermostat-humidity-fan-heating-only-nostate-nobattery-aqs-pm10-pm25-ch2o-meas-pm10-pm25-ch2o-no2-tvoc-level.yml} (86%) rename drivers/SmartThings/matter-thermostat/profiles/{air-purifier-hepa-ac-aqs-co2-tvoc-meas-co2-radon-level.yml => air-purifier-hepa-ac-rock-wind.yml} (64%) create mode 100644 drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-rock.yml create mode 100644 drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-rock-wind.yml create mode 100644 drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-rock.yml create mode 100644 drivers/SmartThings/matter-thermostat/profiles/air-purifier-rock-wind.yml create mode 100644 drivers/SmartThings/matter-thermostat/profiles/air-purifier-rock.yml create mode 100644 drivers/SmartThings/matter-thermostat/profiles/fan-generic.yml create mode 100644 drivers/SmartThings/matter-thermostat/profiles/fan-rock-wind.yml create mode 100644 drivers/SmartThings/matter-thermostat/profiles/fan-rock.yml create mode 100644 drivers/SmartThings/matter-thermostat/profiles/fan-wind.yml create mode 100644 drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index fec18a21b9..b7ab19cb91 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -76,7 +76,7 @@ matterGeneric: deviceLabel: Matter Fan deviceTypes: - id: 0x002B - deviceProfileName: fan + deviceProfileName: fan-generic - id: "matter/air-purifier" deviceLabel: Matter Air Purifier deviceTypes: diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-ac-rock-wind.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-ac-rock-wind.yml new file mode 100644 index 0000000000..55f9910fad --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-ac-rock-wind.yml @@ -0,0 +1,28 @@ +name: air-purifier-ac-rock-wind +components: +- id: main + label: Main + capabilities: + - id: airPurifierFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: fanOscillationMode + version: 1 + - id: windMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirPurifier +- id: activatedCarbonFilter + label: Activated carbon filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: AirPurifier diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-ac-rock.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-ac-rock.yml new file mode 100644 index 0000000000..5b9b5fd3d7 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-ac-rock.yml @@ -0,0 +1,26 @@ +name: air-purifier-ac-rock +components: +- id: main + label: Main + capabilities: + - id: airPurifierFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: fanOscillationMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirPurifier +- id: activatedCarbonFilter + label: Activated carbon filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: AirPurifier diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-wind-thermostat-humidity-fan-heating-only-nostate-nobattery-aqs-pm10-pm25-ch2o-meas-pm10-pm25-ch2o-no2-tvoc-level.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-rock-wind-thermostat-humidity-fan-heating-only-nostate-nobattery-aqs-pm10-pm25-ch2o-meas-pm10-pm25-ch2o-no2-tvoc-level.yml similarity index 86% rename from drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-wind-thermostat-humidity-fan-heating-only-nostate-nobattery-aqs-pm10-pm25-ch2o-meas-pm10-pm25-ch2o-no2-tvoc-level.yml rename to drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-rock-wind-thermostat-humidity-fan-heating-only-nostate-nobattery-aqs-pm10-pm25-ch2o-meas-pm10-pm25-ch2o-no2-tvoc-level.yml index 21dc60d1df..6377602972 100644 --- a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-wind-thermostat-humidity-fan-heating-only-nostate-nobattery-aqs-pm10-pm25-ch2o-meas-pm10-pm25-ch2o-no2-tvoc-level.yml +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-rock-wind-thermostat-humidity-fan-heating-only-nostate-nobattery-aqs-pm10-pm25-ch2o-meas-pm10-pm25-ch2o-no2-tvoc-level.yml @@ -1,4 +1,4 @@ -name: air-purifier-hepa-ac-wind-thermostat-humidity-fan-heating-only-nostate-nobattery-aqs-pm10-pm25-ch2o-meas-pm10-pm25-ch2o-no2-tvoc-level +name: air-purifier-hepa-ac-rock-wind-thermostat-humidity-fan-heating-only-nostate-nobattery-aqs-pm10-pm25-ch2o-meas-pm10-pm25-ch2o-no2-tvoc-level components: - id: main label: Main @@ -7,6 +7,8 @@ components: version: 1 - id: fanSpeedPercent version: 1 + - id: fanOscillationMode + version: 1 - id: windMode version: 1 - id: thermostatHeatingSetpoint diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-aqs-co2-tvoc-meas-co2-radon-level.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-rock-wind.yml similarity index 64% rename from drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-aqs-co2-tvoc-meas-co2-radon-level.yml rename to drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-rock-wind.yml index 9bf3605968..76a7cad7d5 100644 --- a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-aqs-co2-tvoc-meas-co2-radon-level.yml +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-rock-wind.yml @@ -1,4 +1,4 @@ -name: air-purifier-hepa-ac-aqs-co2-tvoc-meas-co2-radon-level +name: air-purifier-hepa-ac-rock-wind components: - id: main label: Main @@ -7,15 +7,9 @@ components: version: 1 - id: fanSpeedPercent version: 1 - - id: airQualityHealthConcern + - id: fanOscillationMode version: 1 - - id: carbonDioxideHealthConcern - version: 1 - - id: carbonDioxideMeasurement - version: 1 - - id: radonHealthConcern - version: 1 - - id: tvocMeasurement + - id: windMode version: 1 - id: firmwareUpdate version: 1 @@ -24,7 +18,7 @@ components: categories: - name: AirPurifier - id: hepaFilter - label: Hepa Filter + label: Hepa filter capabilities: - id: filterState version: 1 @@ -33,7 +27,7 @@ components: categories: - name: AirPurifier - id: activatedCarbonFilter - label: Activated Carbon Filter + label: Activated carbon filter capabilities: - id: filterState version: 1 diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-rock.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-rock.yml new file mode 100644 index 0000000000..e32e5aa68d --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-rock.yml @@ -0,0 +1,35 @@ +name: air-purifier-hepa-ac-rock +components: +- id: main + label: Main + capabilities: + - id: airPurifierFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: fanOscillationMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirPurifier +- id: hepaFilter + label: Hepa filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: AirPurifier +- id: activatedCarbonFilter + label: Activated carbon filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: AirPurifier diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-rock-wind.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-rock-wind.yml new file mode 100644 index 0000000000..d93eac02af --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-rock-wind.yml @@ -0,0 +1,28 @@ +name: air-purifier-hepa-rock-wind +components: +- id: main + label: Main + capabilities: + - id: airPurifierFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: fanOscillationMode + version: 1 + - id: windMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirPurifier +- id: hepaFilter + label: Hepa filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: AirPurifier diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-rock.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-rock.yml new file mode 100644 index 0000000000..46cfa0ca9c --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-rock.yml @@ -0,0 +1,26 @@ +name: air-purifier-hepa-rock +components: +- id: main + label: Main + capabilities: + - id: airPurifierFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: fanOscillationMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirPurifier +- id: hepaFilter + label: Hepa filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: AirPurifier diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-rock-wind.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-rock-wind.yml new file mode 100644 index 0000000000..3ced08a409 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-rock-wind.yml @@ -0,0 +1,18 @@ +name: air-purifier-rock-wind +components: +- id: main + capabilities: + - id: airPurifierFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: fanOscillationMode + version: 1 + - id: windMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirPurifier diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-rock.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-rock.yml new file mode 100644 index 0000000000..c35867d2d1 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-rock.yml @@ -0,0 +1,16 @@ +name: air-purifier-rock +components: +- id: main + capabilities: + - id: airPurifierFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: fanOscillationMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirPurifier diff --git a/drivers/SmartThings/matter-thermostat/profiles/fan-generic.yml b/drivers/SmartThings/matter-thermostat/profiles/fan-generic.yml new file mode 100644 index 0000000000..b58204ce69 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/fan-generic.yml @@ -0,0 +1,17 @@ +name: fan-generic +components: +- id: main + capabilities: + - id: airConditionerFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Fan +metadata: + mnmn: SmartThingsEdge + vid: generic-fan diff --git a/drivers/SmartThings/matter-thermostat/profiles/fan-rock-wind.yml b/drivers/SmartThings/matter-thermostat/profiles/fan-rock-wind.yml new file mode 100644 index 0000000000..b87076b9ff --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/fan-rock-wind.yml @@ -0,0 +1,21 @@ +name: fan-rock-wind +components: +- id: main + capabilities: + - id: airConditionerFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: fanOscillationMode + version: 1 + - id: windMode + version: 1 + - 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/profiles/fan-rock.yml b/drivers/SmartThings/matter-thermostat/profiles/fan-rock.yml new file mode 100644 index 0000000000..f4aa4a8654 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/fan-rock.yml @@ -0,0 +1,19 @@ +name: fan-rock +components: +- id: main + capabilities: + - id: airConditionerFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: fanOscillationMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Fan +metadata: + mnmn: SmartThingsEdge + vid: generic-fan-rock diff --git a/drivers/SmartThings/matter-thermostat/profiles/fan-wind.yml b/drivers/SmartThings/matter-thermostat/profiles/fan-wind.yml new file mode 100644 index 0000000000..71a6597406 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/fan-wind.yml @@ -0,0 +1,19 @@ +name: fan-wind +components: +- id: main + capabilities: + - id: airConditionerFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: windMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Fan +metadata: + mnmn: SmartThingsEdge + vid: generic-fan-wind diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index 9e099acbea..f78103f6f4 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -69,6 +69,12 @@ local WIND_MODE_MAP = { [1] = capabilities.windMode.windMode.naturalWind } +local ROCK_MODE_MAP = { + [0] = capabilities.fanOscillationMode.fanOscillationMode.horizontal, + [1] = capabilities.fanOscillationMode.fanOscillationMode.vertical, + [2] = capabilities.fanOscillationMode.fanOscillationMode.swing +} + local RAC_DEVICE_TYPE_ID = 0x0072 local AP_DEVICE_TYPE_ID = 0x002D local FAN_DEVICE_TYPE_ID = 0x002B @@ -128,6 +134,10 @@ local subscribed_attributes = { clusters.FanControl.attributes.WindSupport, clusters.FanControl.attributes.WindSetting }, + [capabilities.fanOscillationMode.ID] = { + clusters.FanControl.attributes.RockSupport, + clusters.FanControl.attributes.RockSetting + }, [capabilities.battery.ID] = { clusters.PowerSource.attributes.BatPercentRemaining }, @@ -326,6 +336,7 @@ local function do_configure(driver, device) local thermo_eps = device:get_endpoints(clusters.Thermostat.ID) local fan_eps = device:get_endpoints(clusters.FanControl.ID) local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) + local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) local battery_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) -- use get_endpoints for embedded clusters @@ -361,7 +372,19 @@ local function do_configure(driver, device) device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) device:try_update_metadata({profile = profile_name}) elseif device_type == FAN_DEVICE_TYPE_ID then - device.log.warn_with({hub_logs=true}, "Fan supports only one profile") + profile_name = "fan" + if #rock_eps > 0 then + profile_name = profile_name .. "-rock" + end + if #wind_eps > 0 then + profile_name = profile_name .. "-wind" + end + if profile_name == "fan" then + profile_name = "fan-generic" + end + + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) elseif device_type == AP_DEVICE_TYPE_ID then profile_name = "air-purifier" if #hepa_filter_eps > 0 and #ac_filter_eps > 0 then @@ -371,6 +394,9 @@ local function do_configure(driver, device) elseif #ac_filter_eps > 0 then profile_name = profile_name .. "-ac" end + if #rock_eps > 0 then + profile_name = profile_name .. "-rock" + end if #wind_eps > 0 then profile_name = profile_name .. "-wind" end @@ -477,9 +503,12 @@ local function do_configure(driver, device) end local function device_added(driver, device) - device:send(clusters.Thermostat.attributes.ControlSequenceOfOperation:read(device)) - device:send(clusters.FanControl.attributes.FanModeSequence:read(device)) - device:send(clusters.FanControl.attributes.WindSupport:read(device)) + local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + req:merge(clusters.Thermostat.attributes.ControlSequenceOfOperation:read(device)) + req:merge(clusters.FanControl.attributes.FanModeSequence:read(device)) + req:merge(clusters.FanControl.attributes.WindSupport:read(device)) + req:merge(clusters.FanControl.attributes.RockSupport:read(device)) + device:send(req) end local function store_unit_factory(capability_name) @@ -897,6 +926,27 @@ local function wind_setting_handler(driver, device, ib, response) device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windMode.windMode.noWind()) end +local function rock_support_handler(driver, device, ib, response) + local supported_rock_modes = {capabilities.fanOscillationMode.fanOscillationMode.off.NAME} + for mode, rock_mode in pairs(ROCK_MODE_MAP) do + if ((ib.data.value >> mode) & 1) > 0 then + table.insert(supported_rock_modes, rock_mode.NAME) + end + end + local event = capabilities.fanOscillationMode.supportedFanOscillationModes(supported_rock_modes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +local function rock_setting_handler(driver, device, ib, response) + for index, rock_mode in pairs(ROCK_MODE_MAP) do + if ((ib.data.value >> index) & 1) > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, rock_mode()) + return + end + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanOscillationMode.fanOscillationMode.off()) +end + local function hepa_filter_condition_handler(driver, device, ib, response) local component = device.profile.components["hepaFilter"] local condition = ib.data.value @@ -1124,6 +1174,18 @@ local function set_wind_mode(driver, device, cmd) device:send(clusters.FanControl.attributes.WindSetting:write(device, device:component_to_endpoint(cmd.component), wind_mode)) end +local function set_rock_mode(driver, device, cmd) + local rock_mode = 0 + if cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.horizontal.NAME then + rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_LEFT_RIGHT + elseif cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.vertical.NAME then + rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_UP_DOWN + elseif cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.swing.NAME then + rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_ROUND + end + device:send(clusters.FanControl.attributes.RockSetting:write(device, device:component_to_endpoint(cmd.component), rock_mode)) +end + local function battery_percent_remaining_attr_handler(driver, device, ib, response) if ib.data.value then device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) @@ -1160,7 +1222,9 @@ local matter_driver_template = { [clusters.FanControl.attributes.FanMode.ID] = fan_mode_handler, [clusters.FanControl.attributes.PercentCurrent.ID] = fan_speed_percent_attr_handler, [clusters.FanControl.attributes.WindSupport.ID] = wind_support_handler, - [clusters.FanControl.attributes.WindSetting.ID] = wind_setting_handler + [clusters.FanControl.attributes.WindSetting.ID] = wind_setting_handler, + [clusters.FanControl.attributes.RockSupport.ID] = rock_support_handler, + [clusters.FanControl.attributes.RockSetting.ID] = rock_setting_handler, }, [clusters.TemperatureMeasurement.ID] = { [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = temp_event_handler(capabilities.temperatureMeasurement.temperature), @@ -1270,6 +1334,9 @@ local matter_driver_template = { }, [capabilities.windMode.ID] = { [capabilities.windMode.commands.setWindMode.NAME] = set_wind_mode, + }, + [capabilities.fanOscillationMode.ID] = { + [capabilities.fanOscillationMode.commands.setFanOscillationMode.NAME] = set_rock_mode, } }, supported_capabilities = { @@ -1282,6 +1349,7 @@ local matter_driver_template = { capabilities.fanSpeedPercent, capabilities.airPurifierFanMode, capabilities.windMode, + capabilities.fanOscillationMode, capabilities.battery, capabilities.filterState, capabilities.filterStatus, diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua index 2e468b5071..55155f650a 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua @@ -59,6 +59,33 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) +local mock_device_rock = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("air-purifier-hepa-ac-rock-wind.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"}, + {cluster_id = clusters.HepaFilterMonitoring.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ActivatedCarbonFilterMonitoring.ID, cluster_type = "SERVER"}, + } + } + } +}) + local mock_device_ap_aqs = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("air-purifier-hepa-ac-wind.yml"), manufacturer_info = { @@ -173,7 +200,7 @@ local mock_device_ap_thermo_aqs = test.mock_device.build_test_matter_device({ }) local mock_device_ap_thermo_aqs_preconfigured = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("air-purifier-hepa-ac-wind-thermostat-humidity-fan-heating-only-nostate-nobattery-aqs-pm10-pm25-ch2o-meas-pm10-pm25-ch2o-no2-tvoc-level.yml"), + profile = t_utils.get_profile_definition("air-purifier-hepa-ac-rock-wind-thermostat-humidity-fan-heating-only-nostate-nobattery-aqs-pm10-pm25-ch2o-meas-pm10-pm25-ch2o-no2-tvoc-level.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -255,6 +282,20 @@ local cluster_subscribe_list = { clusters.ActivatedCarbonFilterMonitoring.attributes.Condition, } +local cluster_subscribe_list_rock = { + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.WindSetting, + clusters.FanControl.attributes.RockSupport, + clusters.FanControl.attributes.RockSetting, + clusters.HepaFilterMonitoring.attributes.ChangeIndication, + clusters.HepaFilterMonitoring.attributes.Condition, + clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication, + clusters.ActivatedCarbonFilterMonitoring.attributes.Condition, +} + local cluster_subscribe_list_configured = { [capabilities.temperatureMeasurement.ID] = { clusters.Thermostat.attributes.LocalTemperature, @@ -286,7 +327,9 @@ local cluster_subscribe_list_configured = { }, [capabilities.windMode.ID] = { clusters.FanControl.attributes.WindSupport, - clusters.FanControl.attributes.WindSetting + clusters.FanControl.attributes.WindSetting, + clusters.FanControl.attributes.RockSupport, + clusters.FanControl.attributes.RockSetting, }, [capabilities.filterState.ID] = { clusters.HepaFilterMonitoring.attributes.Condition, @@ -335,6 +378,15 @@ local function test_init() end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) + + subscribe_request = cluster_subscribe_list_rock[1]:subscribe(mock_device_rock) + for i, cluster in ipairs(cluster_subscribe_list_rock) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_rock)) + end + end + test.socket.matter:__expect_send({mock_device_rock.id, subscribe_request}) + test.mock_device.add_test_device(mock_device_rock) end test.set_test_init_function(test_init) @@ -392,7 +444,7 @@ test.register_coroutine_test( local read_limits = clusters.Thermostat.attributes.AbsMinHeatSetpointLimit:read() read_limits:merge(clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit:read()) test.socket.matter:__expect_send({mock_device_ap_thermo_aqs.id, read_limits}) - mock_device_ap_thermo_aqs:expect_metadata_update({ profile = "air-purifier-hepa-ac-wind-thermostat-humidity-fan-heating-only-nostate-nobattery-aqs-pm10-pm25-ch2o-meas-pm10-pm25-ch2o-no2-tvoc-level" }) + mock_device_ap_thermo_aqs:expect_metadata_update({ profile = "air-purifier-hepa-ac-rock-wind-thermostat-humidity-fan-heating-only-nostate-nobattery-aqs-pm10-pm25-ch2o-meas-pm10-pm25-ch2o-no2-tvoc-level" }) mock_device_ap_thermo_aqs:expect_metadata_update({ provisioning_state = "PROVISIONED" }) print(mock_device_ap_thermo_aqs.profile) end, @@ -726,4 +778,72 @@ test.register_message_test( } ) + +local supportedFanRock = { + capabilities.fanOscillationMode.fanOscillationMode.off.NAME, + capabilities.fanOscillationMode.fanOscillationMode.horizontal.NAME, + capabilities.fanOscillationMode.fanOscillationMode.vertical.NAME, + capabilities.fanOscillationMode.fanOscillationMode.swing.NAME +} +test.register_message_test( + "Test rock mode", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device_rock.id, + clusters.FanControl.attributes.RockSupport:build_test_report_data(mock_device_rock, 1, 0x07) -- off, RockLeftRight (0x01), RockUpDown (0x02), and RockRound (0x04) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_rock:generate_test_message("main", capabilities.fanOscillationMode.supportedFanOscillationModes(supportedFanRock, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device_rock.id, + clusters.FanControl.attributes.RockSetting:build_test_report_data(mock_device_rock, 1, clusters.FanControl.types.RockBitmap.ROCK_UP_DOWN) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_rock:generate_test_message("main", capabilities.fanOscillationMode.fanOscillationMode.vertical()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device_rock.id, + clusters.FanControl.attributes.RockSetting:build_test_report_data(mock_device_rock, 1, clusters.FanControl.types.RockBitmap.ROCK_LEFT_RIGHT) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_rock:generate_test_message("main", capabilities.fanOscillationMode.fanOscillationMode.horizontal()) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device_rock.id, + { capability = "fanOscillationMode", component = "main", command = "setFanOscillationMode", args = { "vertical" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device_rock.id, + clusters.FanControl.attributes.RockSetting:write(mock_device_rock, 1, clusters.FanControl.types.RockBitmap.ROCK_UP_DOWN) + } + } + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua new file mode 100644 index 0000000000..5dda220f4f --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua @@ -0,0 +1,132 @@ +-- Copyright 2024 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 t_utils = require "integration_test.utils" + +local clusters = require "st.matter.clusters" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("fan-rock-wind.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}, + }, + device_types = { + {device_type_id = 0x002B, device_type_revision = 1,} -- Fan + } + } + } +}) + +local mock_device_generic = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("fan-generic.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 = 0}, + }, + device_types = { + {device_type_id = 0x002B, device_type_revision = 1,} -- Fan + } + } + } +}) + +local cluster_subscribe_list = { + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.WindSetting, + clusters.FanControl.attributes.RockSupport, + clusters.FanControl.attributes.RockSetting, +} + +local cluster_subscribe_list_generic = { + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.PercentCurrent, +} + +local function test_init() + local 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.matter:__expect_send({mock_device.id, subscribe_request}) + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +local function test_init_generic() + local subscribe_request = cluster_subscribe_list_generic[1]:subscribe(mock_device_generic) + for i, cluster in ipairs(cluster_subscribe_list_generic) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_generic)) + end + end + test.socket.matter:__expect_send({mock_device_generic.id, subscribe_request}) + test.mock_device.add_test_device(mock_device_generic) +end + +test.register_coroutine_test( + "Test profile change on fan with rock and wind", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "fan-rock-wind" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { test_init = test_init } +) + +test.register_coroutine_test( + "Test profile change on fan with no features", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_generic.id, "doConfigure" }) + mock_device_generic:expect_metadata_update({ profile = "fan-generic" }) + mock_device_generic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { test_init = test_init_generic } +) + +test.run_registered_tests()