diff --git a/drivers/SmartThings/zigbee-thermostat/fingerprints.yml b/drivers/SmartThings/zigbee-thermostat/fingerprints.yml index d1ced3a2cf..ec47dcddf3 100644 --- a/drivers/SmartThings/zigbee-thermostat/fingerprints.yml +++ b/drivers/SmartThings/zigbee-thermostat/fingerprints.yml @@ -1,4 +1,9 @@ zigbeeManufacturer: + - id: "Vimar/02973-smart-wheel-thermostat" + deviceLabel: Vimar Smart Thermostat + manufacturer: Vimar + model: WheelThermostat_v1.0 + deviceProfileName: thermostat-fanless-heating-no-fw - id: "LUX/KONOZ" deviceLabel: LUX Thermostat manufacturer: LUX diff --git a/drivers/SmartThings/zigbee-thermostat/profiles/thermostat-fanless-cooling-no-fw.yml b/drivers/SmartThings/zigbee-thermostat/profiles/thermostat-fanless-cooling-no-fw.yml new file mode 100644 index 0000000000..2ca894acbf --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/profiles/thermostat-fanless-cooling-no-fw.yml @@ -0,0 +1,27 @@ +name: thermostat-fanless-cooling-no-fw +components: + - id: main + categories: + - name: Thermostat + capabilities: + - id: temperatureMeasurement + version: 1 + - id: thermostatCoolingSetpoint + version: 1 + config: + values: + - key: "coolingSetpoint.value" + range: [6, 40] + step: 0.1 + - id: thermostatOperatingState + version: 1 + config: + values: + - key: "thermostatOperatingState.value" + enabledValues: + - cooling + - idle + - id: thermostatMode + version: 1 + - id: refresh + version: 1 diff --git a/drivers/SmartThings/zigbee-thermostat/profiles/thermostat-fanless-heating-no-fw.yml b/drivers/SmartThings/zigbee-thermostat/profiles/thermostat-fanless-heating-no-fw.yml new file mode 100644 index 0000000000..ace3283799 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/profiles/thermostat-fanless-heating-no-fw.yml @@ -0,0 +1,27 @@ +name: thermostat-fanless-heating-no-fw +components: + - id: main + categories: + - name: Thermostat + capabilities: + - id: temperatureMeasurement + version: 1 + - id: thermostatHeatingSetpoint + version: 1 + config: + values: + - key: "heatingSetpoint.value" + range: [5, 39] + step: 0.1 + - id: thermostatOperatingState + version: 1 + config: + values: + - key: "thermostatOperatingState.value" + enabledValues: + - heating + - idle + - id: thermostatMode + version: 1 + - id: refresh + version: 1 diff --git a/drivers/SmartThings/zigbee-thermostat/src/init.lua b/drivers/SmartThings/zigbee-thermostat/src/init.lua index 0401e03a28..d323a7e399 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/init.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- 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. @@ -292,7 +292,8 @@ local zigbee_thermostat_driver = { require("stelpro"), require("lux-konoz"), require("leviton"), - require("popp_danfoss") + require("popp_danfoss"), + require("vimar") }, } diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_vimar_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_vimar_thermostat.lua new file mode 100644 index 0000000000..32a089680b --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_vimar_thermostat.lua @@ -0,0 +1,813 @@ +-- 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 clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local Thermostat = clusters.Thermostat +local ThermostatMode = capabilities.thermostatMode +local SystemMode = Thermostat.attributes.SystemMode +local ThermostatControlSequence = Thermostat.attributes.ControlSequenceOfOperation + +local MAX_VIMAR_THERMOSTAT_HEATPOINT_LIMIT = 39 +local MAX_VIMAR_THERMOSTAT_HEATPOINT_LIMIT_ZIGBEE = 3900 +local MIN_VIMAR_THERMOSTAT_HEATPOINT_LIMIT = 5 +local MIN_VIMAR_THERMOSTAT_HEATPOINT_LIMIT_ZIGBEE = 500 +local MAX_VIMAR_THERMOSTAT_COOLPOINT_LIMIT = 40 +local MAX_VIMAR_THERMOSTAT_COOLPOINT_LIMIT_ZIGBEE = 4000 +local MIN_VIMAR_THERMOSTAT_COOLPOINT_LIMIT = 6 +local MIN_VIMAR_THERMOSTAT_COOLPOINT_LIMIT_ZIGBEE = 600 + +local VIMAR_CURRENT_PROFILE = "_vimarThermostatCurrentProfile" + +local VIMAR_THERMOSTAT_HEATING_PROFILE = "thermostat-fanless-heating-no-fw" +local VIMAR_THERMOSTAT_COOLING_PROFILE = "thermostat-fanless-cooling-no-fw" + +local mock_device_vimar_heating = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition(VIMAR_THERMOSTAT_HEATING_PROFILE .. ".yml"), + zigbee_endpoints = { + [10] = { + id = 10, + manufacturer = "Vimar", + model = "WheelThermostat_v1.0", + server_clusters = { 0x0000, 0x0003, 0x0201 } + } + } + } +) + +local mock_device_vimar_cooling = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition(VIMAR_THERMOSTAT_COOLING_PROFILE .. ".yml"), + zigbee_endpoints = { + [10] = { + id = 10, + manufacturer = "Vimar", + model = "WheelThermostat_v1.0", + server_clusters = { 0x0000, 0x0003, 0x0201 } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + mock_device_vimar_heating:set_field(VIMAR_CURRENT_PROFILE, ThermostatMode.thermostatMode.heat.NAME, { persist = true }) + mock_device_vimar_cooling:set_field(VIMAR_CURRENT_PROFILE, ThermostatMode.thermostatMode.cool.NAME, { persist = true }) + test.mock_device.add_test_device(mock_device_vimar_heating) + test.mock_device.add_test_device(mock_device_vimar_cooling) + zigbee_test_utils.init_noop_health_check_timer() +end + +test.set_test_init_function(test_init) + + +-- Test (Device -> SmartThings) +-- temperatureMeasurement +-- ========================================================= +test.register_message_test( + "Vimar Thermostat - LocalTemperature reporting is handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device_vimar_heating.id, + Thermostat.attributes.LocalTemperature:build_test_attr_report(mock_device_vimar_heating, 2500) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_vimar_heating:generate_test_message( + "main", + capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }) + ) + } + } +) + +-- Test (Device -> SmartThings) +-- thermostatHeatingSetpoint +-- ========================================================= +test.register_message_test( + "Vimar Thermostat - Heating setpoint reporting should handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device_vimar_heating.id, + Thermostat.attributes.OccupiedHeatingSetpoint:build_test_attr_report( + mock_device_vimar_heating, + 3000 + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_vimar_heating:generate_test_message( + "main", + capabilities.thermostatHeatingSetpoint.heatingSetpoint({ value = 30.0, unit = "C" }) + ) + } + } +) + +-- Test (Device -> SmartThings) +-- thermostatCoolingSetpoint +-- ========================================================= +test.register_message_test( + "Vimar Thermostat - Cooling setpoint reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device_vimar_cooling.id, + Thermostat.attributes.OccupiedCoolingSetpoint:build_test_attr_report( + mock_device_vimar_cooling, + 1800 + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_vimar_cooling:generate_test_message( + "main", + capabilities.thermostatCoolingSetpoint.coolingSetpoint({ value = 18.0, unit = "C" }) + ) + } + } +) + +-- Test (SmartThings -> Device) +-- thermostatCoolingSetpoint normal condition +-- ========================================================= +test.register_coroutine_test( + "Vimar Thermostat - Setting cooling setpoint should generate correct zigbee messages", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive( + { + mock_device_vimar_cooling.id, + { + capability = "thermostatCoolingSetpoint", + component = "main", + command = "setCoolingSetpoint", + args = { 27 } + } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_cooling.id, + Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device_vimar_cooling, 2700) + } + ) + test.wait_for_events() + + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_cooling.id, + Thermostat.attributes.OccupiedCoolingSetpoint:read(mock_device_vimar_cooling) + } + ) + end +) + +-- Test (SmartThings -> Device) +-- thermostatCoolingSetpoint Fahrenheit conversion +-- ========================================================= +test.register_coroutine_test( + "Vimar Thermostat - Setting cooling setpoint with a Fahrenheit value should generate correct zigbee messages", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive( + { + mock_device_vimar_cooling.id, + { + capability = "thermostatCoolingSetpoint", + component = "main", + command = "setCoolingSetpoint", + args = { 69 } + } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_cooling.id, + Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device_vimar_cooling, 2056) + } + ) + test.wait_for_events() + + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_cooling.id, + Thermostat.attributes.OccupiedCoolingSetpoint:read(mock_device_vimar_cooling) + } + ) + end +) + +-- Test (SmartThings -> Device) +-- thermostatCoolingSetpoint MAX limit +-- ========================================================= +test.register_coroutine_test( + "Vimar Thermostat - Setting cooling setpoint at MAX limit should generate correct zigbee messages", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive( + { + mock_device_vimar_cooling.id, + { + capability = "thermostatCoolingSetpoint", + component = "main", + command = "setCoolingSetpoint", + args = { MAX_VIMAR_THERMOSTAT_COOLPOINT_LIMIT } + } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_cooling.id, + Thermostat.attributes.OccupiedCoolingSetpoint:write( + mock_device_vimar_cooling, + MAX_VIMAR_THERMOSTAT_COOLPOINT_LIMIT_ZIGBEE + ) + } + ) + test.wait_for_events() + + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_cooling.id, + Thermostat.attributes.OccupiedCoolingSetpoint:read(mock_device_vimar_cooling) + } + ) + end +) + +-- Test (SmartThings -> Device) +-- thermostatCoolingSetpoint MIN limit +-- ========================================================= +test.register_coroutine_test( + "Vimar Thermostat - Setting cooling setpoint at MIN limit should generate correct zigbee messages", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive( + { + mock_device_vimar_cooling.id, + { + capability = "thermostatCoolingSetpoint", + component = "main", + command = "setCoolingSetpoint", + args = { MIN_VIMAR_THERMOSTAT_COOLPOINT_LIMIT } + } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_cooling.id, + Thermostat.attributes.OccupiedCoolingSetpoint:write( + mock_device_vimar_cooling, + MIN_VIMAR_THERMOSTAT_COOLPOINT_LIMIT_ZIGBEE + ) + } + ) + test.wait_for_events() + + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_cooling.id, + Thermostat.attributes.OccupiedCoolingSetpoint:read(mock_device_vimar_cooling) + } + ) + end +) + + +-- Test (SmartThings -> Device) +-- thermostatCoolingSetpoint resolution +-- ========================================================= +test.register_coroutine_test( + "Vimar Thermostat - Setting cooling setpoint with 0.1 resolution should be handled", + function() + test.socket.capability:__queue_receive( + { + mock_device_vimar_cooling.id, + { + capability = "thermostatCoolingSetpoint", + component = "main", + command = "setCoolingSetpoint", + args = { 27.0 } + } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_cooling.id, + Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device_vimar_cooling, 2700) + } + ) + test.socket.capability:__queue_receive( + { + mock_device_vimar_cooling.id, + { + capability = "thermostatCoolingSetpoint", + component = "main", + command = "setCoolingSetpoint", + args = { 27.1 } + } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_cooling.id, + Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device_vimar_cooling, 2710) + } + ) + test.socket.capability:__queue_receive( + { + mock_device_vimar_cooling.id, + { + capability = "thermostatCoolingSetpoint", + component = "main", + command = "setCoolingSetpoint", + args = { 27.2 } + } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_cooling.id, + Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device_vimar_cooling, 2720) + } + ) + end +) + + +-- Test (SmartThings -> Device) +-- thermostatHeatingSetpoint normal condition +-- ========================================================= +test.register_coroutine_test( + "Vimar Thermostat - Setting heating setpoint should generate correct zigbee messages", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive( + { + mock_device_vimar_heating.id, + { + capability = "thermostatHeatingSetpoint", + component = "main", + command = "setHeatingSetpoint", + args = { 23 } + } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_heating.id, + Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device_vimar_heating, 2300) + } + ) + test.wait_for_events() + + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_heating.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device_vimar_heating) + } + ) + end +) + +-- Test (SmartThings -> Device) +-- thermostatHeatingSetpoint Fahrenheit conversion +-- ========================================================= +test.register_coroutine_test( + "Vimar Thermostat - Setting heating setpoint with a Fahrenheit value should generate correct zigbee messages", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive( + { + mock_device_vimar_heating.id, + { + capability = "thermostatHeatingSetpoint", + component = "main", + command = "setHeatingSetpoint", + args = { 48 } + } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_heating.id, + Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device_vimar_heating, 889) + } + ) + test.wait_for_events() + + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_heating.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device_vimar_heating) + } + ) + end +) + +-- Test (SmartThings -> Device) +-- thermostatHeatingSetpoint MAX limit +-- ========================================================= +test.register_coroutine_test( + "Vimar Thermostat - Setting heating setpoint at MAX limit should generate correct zigbee messages", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive( + { + mock_device_vimar_heating.id, + { + capability = "thermostatHeatingSetpoint", + component = "main", + command = "setHeatingSetpoint", + args = { MAX_VIMAR_THERMOSTAT_HEATPOINT_LIMIT } + } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_heating.id, + Thermostat.attributes.OccupiedHeatingSetpoint:write( + mock_device_vimar_heating, + MAX_VIMAR_THERMOSTAT_HEATPOINT_LIMIT_ZIGBEE + ) + } + ) + test.wait_for_events() + + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_heating.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device_vimar_heating) + } + ) + end +) + +-- Test (SmartThings -> Device) +-- thermostatHeatingSetpoint MIN limit +-- ========================================================= +test.register_coroutine_test( + "Vimar Thermostat - Setting heating setpoint at MIN limit should generate correct zigbee messages", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive( + { + mock_device_vimar_heating.id, + { + capability = "thermostatHeatingSetpoint", + component = "main", + command = "setHeatingSetpoint", + args = { MIN_VIMAR_THERMOSTAT_HEATPOINT_LIMIT } + } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_heating.id, + Thermostat.attributes.OccupiedHeatingSetpoint:write( + mock_device_vimar_heating, + MIN_VIMAR_THERMOSTAT_HEATPOINT_LIMIT_ZIGBEE + ) + } + ) + test.wait_for_events() + + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_heating.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device_vimar_heating) + } + ) + end +) + + +-- Test (SmartThings -> Device) +-- thermostatHeatingSetpoint resolution +-- ========================================================= +test.register_coroutine_test( + "Vimar Thermostat - Setting heating setpoint with 0.1 resolution should be handled", + function() + test.socket.capability:__queue_receive( + { + mock_device_vimar_heating.id, + { + capability = "thermostatHeatingSetpoint", + component = "main", + command = "setHeatingSetpoint", + args = { 19.0 } + } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_heating.id, + Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device_vimar_heating, 1900) + } + ) + test.socket.capability:__queue_receive( + { + mock_device_vimar_heating.id, + { + capability = "thermostatHeatingSetpoint", + component = "main", + command = "setHeatingSetpoint", + args = { 19.1 } + } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_heating.id, + Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device_vimar_heating, 1910) + } + ) + test.socket.capability:__queue_receive( + { + mock_device_vimar_heating.id, + { + capability = "thermostatHeatingSetpoint", + component = "main", + command = "setHeatingSetpoint", + args = { 19.2 } + } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device_vimar_heating.id, + Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device_vimar_heating, 1920) + } + ) + end +) + + +-- Test (Device -> SmartThings) +-- Cooling Only +-- ========================================================= +test.register_message_test( + "Vimar Thermostat - ControlSequenceOfOperation reporting (CoolingOnly) is handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device_vimar_heating.id, + Thermostat.attributes.ControlSequenceOfOperation:build_test_attr_report( + mock_device_vimar_heating, + ThermostatControlSequence.COOLING_ONLY + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_vimar_heating:generate_test_message( + "main", + capabilities.thermostatMode.supportedThermostatModes( + { ThermostatMode.thermostatMode.off.NAME, ThermostatMode.thermostatMode.cool.NAME }, + { visibility = { displayed = false } } + ) + ) + } + } +) + +-- Test (Device -> SmartThings) +-- Heating Only +-- ========================================================= +test.register_message_test( + "Vimar Thermostat - ControlSequenceOfOperation reporting (HeatingOnly) is handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device_vimar_heating.id, + Thermostat.attributes.ControlSequenceOfOperation:build_test_attr_report( + mock_device_vimar_heating, + ThermostatControlSequence.HEATING_ONLY + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_vimar_heating:generate_test_message( + "main", + capabilities.thermostatMode.supportedThermostatModes( + { ThermostatMode.thermostatMode.off.NAME, ThermostatMode.thermostatMode.heat.NAME }, + { visibility = { displayed = false } } + ) + ) + } + } +) + +-- Test (SmartThings -> Device) +-- Refresh +-- ========================================================= +test.register_message_test( + "Vimar Thermostat - Refresh capability should read all required attributes in heating mode", + { + { + channel = "capability", + direction = "receive", + message = { mock_device_vimar_heating.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } } + }, + -- [NOTE:] Strict order + { + channel = "zigbee", + direction = "send", + message = { + mock_device_vimar_heating.id, + Thermostat.attributes.LocalTemperature:read(mock_device_vimar_heating) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device_vimar_heating.id, + Thermostat.attributes.ControlSequenceOfOperation:read(mock_device_vimar_heating) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device_vimar_heating.id, + Thermostat.attributes.ThermostatRunningState:read(mock_device_vimar_heating) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device_vimar_heating.id, + Thermostat.attributes.SystemMode:read(mock_device_vimar_heating) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device_vimar_heating.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device_vimar_heating) + } + } + } +) + + +-- Test (SmartThings -> Device) +-- Refresh +-- ========================================================= +test.register_message_test( + "Vimar Thermostat - Refresh capability should read all required attributes in cooling mode", + { + { + channel = "capability", + direction = "receive", + message = { mock_device_vimar_cooling.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } } + }, + -- [NOTE:] Strict order + { + channel = "zigbee", + direction = "send", + message = { + mock_device_vimar_cooling.id, + Thermostat.attributes.LocalTemperature:read(mock_device_vimar_cooling) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device_vimar_cooling.id, + Thermostat.attributes.ControlSequenceOfOperation:read(mock_device_vimar_cooling) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device_vimar_cooling.id, + Thermostat.attributes.ThermostatRunningState:read(mock_device_vimar_cooling) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device_vimar_cooling.id, + Thermostat.attributes.SystemMode:read(mock_device_vimar_cooling) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device_vimar_cooling.id, + Thermostat.attributes.OccupiedCoolingSetpoint:read(mock_device_vimar_cooling) + } + } + } +) + +-- Test (Device -> SmartThings) +-- SystemMode +-- ========================================================= +test.register_coroutine_test( + "Vimar Thermostat - Thermostat mode (Cool --> Heat) changed using the physical button is handled", + function() + test.socket.zigbee:__queue_receive( + { + mock_device_vimar_cooling.id, + Thermostat.attributes.SystemMode:build_test_attr_report(mock_device_vimar_cooling, SystemMode.HEAT) + } + ) + mock_device_vimar_cooling:expect_metadata_update({ profile = VIMAR_THERMOSTAT_HEATING_PROFILE }) + test.socket.capability:__expect_send( + { + mock_device_vimar_cooling.id, + { + capability_id = "thermostatMode", + component_id = "main", + attribute_id = "thermostatMode", + state = { value = "heat" } + } + } + ) + end +) + +-- Test (Device -> SmartThings) +-- SystemMode +-- ========================================================= +test.register_coroutine_test( + "Vimar Thermostat - Thermostat mode changed (Heat --> Cool) using the physical button is handled", + function() + test.socket.zigbee:__queue_receive( + { + mock_device_vimar_heating.id, + Thermostat.attributes.SystemMode:build_test_attr_report(mock_device_vimar_heating, SystemMode.COOL) + } + ) + mock_device_vimar_heating:expect_metadata_update({ profile = VIMAR_THERMOSTAT_COOLING_PROFILE }) + test.socket.capability:__expect_send( + { + mock_device_vimar_heating.id, + { + capability_id = "thermostatMode", + component_id = "main", + attribute_id = "thermostatMode", + state = { value = "cool" } + } + } + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-thermostat/src/vimar/init.lua b/drivers/SmartThings/zigbee-thermostat/src/vimar/init.lua new file mode 100644 index 0000000000..b4070cf569 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/vimar/init.lua @@ -0,0 +1,188 @@ +-- 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 device_management = require "st.zigbee.device_management" +local clusters = require "st.zigbee.zcl.clusters" +local utils = require "st.utils" +local Thermostat = clusters.Thermostat +local ThermostatControlSequence = Thermostat.attributes.ControlSequenceOfOperation +local ThermostatSystemMode = Thermostat.attributes.SystemMode +local capabilities = require "st.capabilities" +local ThermostatMode = capabilities.thermostatMode + +local VIMAR_SUPPORTED_THERMOSTAT_MODES = { + [ThermostatControlSequence.COOLING_ONLY] = { + ThermostatMode.thermostatMode.off.NAME, + ThermostatMode.thermostatMode.cool.NAME + }, + [ThermostatControlSequence.HEATING_ONLY] = { + ThermostatMode.thermostatMode.off.NAME, + ThermostatMode.thermostatMode.heat.NAME + } +} + +local VIMAR_THERMOSTAT_MODE_MAP = { + [ThermostatSystemMode.OFF] = ThermostatMode.thermostatMode.off, + [ThermostatSystemMode.COOL] = ThermostatMode.thermostatMode.cool, + [ThermostatSystemMode.HEAT] = ThermostatMode.thermostatMode.heat, +} + +local VIMAR_THERMOSTAT_FINGERPRINT = { + mfr = "Vimar", + model = "WheelThermostat_v1.0" +} + +-- NOTE: This is a global variable to use in order to save the current thermostat profile +local VIMAR_CURRENT_PROFILE = "_vimarThermostatCurrentProfile" + +local VIMAR_THERMOSTAT_HEATING_PROFILE = "thermostat-fanless-heating-no-fw" +local VIMAR_THERMOSTAT_COOLING_PROFILE = "thermostat-fanless-cooling-no-fw" + + +local vimar_thermostat_can_handle = function(opts, driver, device) + return device:get_manufacturer() == VIMAR_THERMOSTAT_FINGERPRINT.mfr and + device:get_model() == VIMAR_THERMOSTAT_FINGERPRINT.model +end + +local vimar_thermostat_supported_modes_handler = function(driver, device, supported_modes) + device:emit_event( + ThermostatMode.supportedThermostatModes( + VIMAR_SUPPORTED_THERMOSTAT_MODES[supported_modes.value], + { visibility = { displayed = false } } + ) + ) +end + +-- NOTE: Vimar requires (5-39) and (6-40) as maximum setpoint limits for heating and cooling, respectively. +-- The device firmare adjusts the opposite setpoint value to +-- overcome the ZigBee deadband limits of 1 degree. +-- I.E. Heating Mode --> CoolingSetpoint 40, HeatingSetpoint 20 +-- Cooliing Mode --> CoolingSetpoint 25, HeatingSetpoint 5 +local vimar_set_setpoint_factory = function(setpoint_attribute) + return function(driver, device, command) + local value = command.args.setpoint + if (value >= 41.0) then + value = utils.f_to_c(value) + end + device:send(setpoint_attribute:write(device, utils.round(value * 100))) + + device.thread:call_with_delay(2, function(d) + device:send(setpoint_attribute:read(device)) + end) + end +end + +-- NOTE: unused attributes are not refreshed +local vimar_thermostat_do_refresh = function(self, device) + local attributes = { + Thermostat.attributes.LocalTemperature, + Thermostat.attributes.ControlSequenceOfOperation, + Thermostat.attributes.ThermostatRunningState, + Thermostat.attributes.SystemMode + } + + local vimar_thermostat_profile = device:get_field(VIMAR_CURRENT_PROFILE) + if vimar_thermostat_profile == ThermostatMode.thermostatMode.heat.NAME then + attributes[#attributes + 1] = Thermostat.attributes.OccupiedHeatingSetpoint + elseif vimar_thermostat_profile == ThermostatMode.thermostatMode.cool.NAME then + attributes[#attributes + 1] = Thermostat.attributes.OccupiedCoolingSetpoint + end + + for _, attribute in pairs(attributes) do + device:send(attribute:read(device)) + end +end + +-- NOTE: Whenever the physical button for the current mode is pressed on the device, this function changes the device profile. +-- If the Thermostat is OFF, no profile change is required. +local vimar_thermostat_mode_handler = function(driver, device, thermostat_mode) + local mode = VIMAR_THERMOSTAT_MODE_MAP[thermostat_mode.value].NAME + -- If is a known supported mode, then apply the change + if VIMAR_THERMOSTAT_MODE_MAP[thermostat_mode.value] then + local vimar_thermostat_profile = device:get_field(VIMAR_CURRENT_PROFILE) + -- HEAT: if the previous mode was cool, update profile + if mode == ThermostatMode.thermostatMode.heat.NAME then + if mode ~= vimar_thermostat_profile then + device:try_update_metadata({ profile = VIMAR_THERMOSTAT_HEATING_PROFILE }) + device:set_field(VIMAR_CURRENT_PROFILE, mode) + device.thread:call_with_delay(2, function(d) + vimar_thermostat_do_refresh(driver, device) + end) + end + -- COOL: if the previous mode was heat, update profile + elseif mode == ThermostatMode.thermostatMode.cool.NAME then + if mode ~= vimar_thermostat_profile then + device:try_update_metadata({ profile = VIMAR_THERMOSTAT_COOLING_PROFILE }) + device:set_field(VIMAR_CURRENT_PROFILE, mode) + device.thread:call_with_delay(2, function(d) + vimar_thermostat_do_refresh(driver, device) + end) + end + end + device:emit_event(VIMAR_THERMOSTAT_MODE_MAP[thermostat_mode.value]()) + end +end + +-- NOTE: override default thermostat mode map; logic is the same from the original driver +local vimar_set_thermostat_mode = function(driver, device, command) + for zigbee_attr_val, st_cap_val in pairs(VIMAR_THERMOSTAT_MODE_MAP) do + if command.args.mode == st_cap_val.NAME then + device:send_to_component(command.component, Thermostat.attributes.SystemMode:write(device, zigbee_attr_val)) + device.thread:call_with_delay(1, function(d) + device:send_to_component(command.component, Thermostat.attributes.SystemMode:read(device)) + end) + break + end + end +end + +-- NOTE: unused binds are not required in the configuration procedure +local vimar_thermostat_do_configure = function(self, device) + device:send(device_management.build_bind_request(device, Thermostat.ID, self.environment_info.hub_zigbee_eui)) + -- Default mode is HEAT + device:set_field(VIMAR_CURRENT_PROFILE, ThermostatMode.thermostatMode.heat.NAME) + -- Read the SystemMode at first configuration so as to change the profile accordingly + -- The profile is changed in vimar_thermostat_mode_handler function + device:send(Thermostat.attributes.SystemMode:read(device)) +end + +local vimar_thermostat_subdriver = { + NAME = "Vimar Thermostat Handler", + zigbee_handlers = { + attr = { + [Thermostat.ID] = { + [Thermostat.attributes.SystemMode.ID] = vimar_thermostat_mode_handler, + [Thermostat.attributes.ControlSequenceOfOperation.ID] = vimar_thermostat_supported_modes_handler, + } + } + }, + capability_handlers = { + [ThermostatMode.ID] = { + [ThermostatMode.commands.setThermostatMode.NAME] = vimar_set_thermostat_mode, + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = vimar_thermostat_do_refresh, + }, + [capabilities.thermostatCoolingSetpoint.ID] = { + [capabilities.thermostatCoolingSetpoint.commands.setCoolingSetpoint.NAME] = vimar_set_setpoint_factory(clusters.Thermostat.attributes.OccupiedCoolingSetpoint) + }, + [capabilities.thermostatHeatingSetpoint.ID] = { + [capabilities.thermostatHeatingSetpoint.commands.setHeatingSetpoint.NAME] = vimar_set_setpoint_factory(clusters.Thermostat.attributes.OccupiedHeatingSetpoint) + } + }, + doConfigure = vimar_thermostat_do_configure, + can_handle = vimar_thermostat_can_handle +} + +return vimar_thermostat_subdriver