From f628c546b261fabe9ae3bbdd008017f226a3f2f9 Mon Sep 17 00:00:00 2001 From: thinkaName <962679819@qq.com> Date: Thu, 14 May 2026 14:17:24 +0800 Subject: [PATCH] add wallhero_thermostat_ACL-403STC1 --- .../zigbee-thermostat/fingerprints.yml | 5 + .../thermostat-thirty-buttons-wallhero.yml | 249 ++++ .../zigbee-thermostat/src/init.lua | 4 +- .../zigbee-thermostat/src/sub_drivers.lua | 1 + .../test_zigbee_thermostat_wallhero_3in1.lua | 1117 +++++++++++++++++ .../src/wallhero/can_handle.lua | 13 + .../src/wallhero/fingerprints.lua | 6 + .../zigbee-thermostat/src/wallhero/init.lua | 233 ++++ tools/localizations/cn.csv | 1 + 9 files changed, 1628 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/zigbee-thermostat/profiles/thermostat-thirty-buttons-wallhero.yml create mode 100644 drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat_wallhero_3in1.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/wallhero/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/wallhero/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/wallhero/init.lua diff --git a/drivers/SmartThings/zigbee-thermostat/fingerprints.yml b/drivers/SmartThings/zigbee-thermostat/fingerprints.yml index d88b3ff89a..efa89be552 100644 --- a/drivers/SmartThings/zigbee-thermostat/fingerprints.yml +++ b/drivers/SmartThings/zigbee-thermostat/fingerprints.yml @@ -134,6 +134,11 @@ zigbeeManufacturer: manufacturer: LUMI model: lumi.airrtc.agl001 deviceProfileName: thermostat-aqara + - id: "WALL HERO/ACL-403STC1" + deviceLabel: WALLHERO Thermostat ACL-403STC1 + manufacturer: WALL HERO + model: ACL-403STC1 + deviceProfileName: thermostat-thirty-buttons-wallhero zigbeeGeneric: - id: "genericThermostat" deviceLabel: Zigbee Thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/profiles/thermostat-thirty-buttons-wallhero.yml b/drivers/SmartThings/zigbee-thermostat/profiles/thermostat-thirty-buttons-wallhero.yml new file mode 100644 index 0000000000..494478fbd9 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/profiles/thermostat-thirty-buttons-wallhero.yml @@ -0,0 +1,249 @@ +name: thermostat-thirty-buttons-wallhero +components: + - id: main + label: 空调 + capabilities: + - id: switch + version: 1 + - id: temperatureMeasurement + version: 1 + - id: temperatureSetpoint + version: 1 + - id: thermostatMode + version: 1 + - id: fanMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: fan + label: 新风 + capabilities: + - id: switch + version: 1 + - id: fanMode + version: 1 + categories: + - name: Fan + - id: heat + label: 地暖 + capabilities: + - id: switch + version: 1 + - id: thermostatHeatingSetpoint + version: 1 + categories: + - name: Thermostat + - id: button1 + label: 回家 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + label: 离家 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + label: 晨起 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + label: 睡眠 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + label: 阅读 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + label: 观影 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button7 + label: 全开 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button8 + label: 全关 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button9 + label: 空调温度调高 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button10 + label: 空调温度降低 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button11 + label: 开窗帘 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button12 + label: 关窗帘 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button13 + label: 空调开 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button14 + label: 空调关 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button15 + label: 无风开 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button16 + label: 无风关 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button17 + label: 制冷 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button18 + label: 制热 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button19 + label: 新风开 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button20 + label: 新风关 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button21 + label: 开电视 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button22 + label: 关电视 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button23 + label: 开音乐 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button24 + label: 关音乐 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button25 + label: 开纱帘 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button26 + label: 关纱帘 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button27 + label: 洗衣服 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button28 + label: 衣服护理 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button29 + label: 用餐 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button30 + label: 开大门 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/zigbee-thermostat/src/init.lua b/drivers/SmartThings/zigbee-thermostat/src/init.lua index a72b3c9107..cfbbd76a0c 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/init.lua @@ -41,7 +41,9 @@ local THERMOSTAT_MODE_MAP = { [ThermostatSystemMode.AUTO] = ThermostatMode.thermostatMode.auto, [ThermostatSystemMode.COOL] = ThermostatMode.thermostatMode.cool, [ThermostatSystemMode.HEAT] = ThermostatMode.thermostatMode.heat, - [ThermostatSystemMode.EMERGENCY_HEATING] = ThermostatMode.thermostatMode.emergency_heat + [ThermostatSystemMode.EMERGENCY_HEATING] = ThermostatMode.thermostatMode.emergency_heat, + [ThermostatSystemMode.DRY] = ThermostatMode.thermostatMode.dryair, + [ThermostatSystemMode.FAN_ONLY] = ThermostatMode.thermostatMode.fanonly } local FAN_MODE_MAP = { diff --git a/drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua b/drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua index 7f62589c24..3b535c3cf0 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua @@ -15,5 +15,6 @@ local sub_drivers = { lazy_load_if_possible("vimar"), lazy_load_if_possible("resideo_korea"), lazy_load_if_possible("aqara"), + lazy_load_if_possible("wallhero"), } return sub_drivers diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat_wallhero_3in1.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat_wallhero_3in1.lua new file mode 100644 index 0000000000..240e6118b3 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat_wallhero_3in1.lua @@ -0,0 +1,1117 @@ +-- Copyright 2026 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. + +-- Mock out globals +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 Scenes = clusters.Scenes +local OnOff = clusters.OnOff +local FanControl = clusters.FanControl +local Thermostat = clusters.Thermostat +local ThermostatMode = capabilities.thermostatMode +local FanMode = capabilities.fanMode +local button_attr = capabilities.button.button + +local SUPPORTED_FAN_MODES = { + { "auto", "high", "medium", "low"}, +} + +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("thermostat-thirty-buttons-wallhero.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "WALL HERO", + model = "ACL-403STC1", + server_clusters = { 0x0005,0x0006,0x0201,0x0202, 0x0203 } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) + zigbee_test_utils.init_noop_health_check_timer() +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "added lifecycle event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + ThermostatMode.supportedThermostatModes({"cool", "dryair", "fanonly", "heat"}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + FanMode.supportedFanModes( SUPPORTED_FAN_MODES[1] , { visibility = { displayed = false }}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "fan", + FanMode.supportedFanModes( SUPPORTED_FAN_MODES[1] , { visibility = { displayed = false }}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.temperatureSetpoint.temperatureSetpointRange({ value = { minimum = 16.00, maximum = 32.00 }, unit = "C" }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "heat", + capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = 16.00, maximum = 32.00 }, unit = "C" }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.switch.switch.off() + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "heat", + capabilities.switch.switch.off() + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "fan", + capabilities.switch.switch.off() + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + ThermostatMode.thermostatMode.cool() + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + FanMode.fanMode.auto() + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "fan", + FanMode.fanMode.auto() + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.temperatureSetpoint.temperatureSetpoint({value = 26, unit = "C"}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "heat", + capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 26, unit = "C"}) + ) + ) + + for _, component in pairs(mock_device.profile.components) do + if component.id ~= "main" and component.id ~= "heat" and component.id ~= "fan" then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + component.id, + capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + component.id, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + end + end + + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:read(mock_device):to_endpoint(0x01) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Thermostat.attributes.LocalTemperature:read(mock_device):to_endpoint(0x01) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Thermostat.attributes.OccupiedCoolingSetpoint:read(mock_device):to_endpoint(0x01) + } + ) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + Thermostat.attributes.SystemMode:read(mock_device):to_endpoint(0x01) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + FanControl.attributes.FanMode:read(mock_device):to_endpoint(0x01) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:read(mock_device):to_endpoint(0x02) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + FanControl.attributes.FanMode:read(mock_device):to_endpoint(0x02) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:read(mock_device):to_endpoint(0x03) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device):to_endpoint(0x03) + } + ) + test.socket:set_time_advance_per_select(0.1) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + + +test.register_message_test( + "Reported on status should be handled: on ep 1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + true):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + } + } +) + +test.register_message_test( + "Reported off status should be handled: off ep 1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + false):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.off()) + } + } +) + +test.register_message_test( + "Temperature reports using the temperatureMeasurement should be handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.LocalTemperature:build_test_attr_report(mock_device, + 2500):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + } + } +) + +test.register_message_test( + "Cooling setpoint reports temperatureSetpoint are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.OccupiedCoolingSetpoint:build_test_attr_report(mock_device, + 2500):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureSetpoint.temperatureSetpoint({ value = 25.0, unit = "C" })) + } + } +) + +test.register_message_test( + "Thermostat running mode reports 3 cool are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.ThermostatRunningMode:build_test_attr_report(mock_device, + 3):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode("cool")) + } + } +) + +test.register_message_test( + "Thermostat running mode reports 4 heat are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.ThermostatRunningMode:build_test_attr_report(mock_device, + 4):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode("heat")) + } + } +) + +test.register_message_test( + "Thermostat running mode reports 8 dryair are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.ThermostatRunningMode:build_test_attr_report(mock_device, + 8):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode("dryair")) + } + } +) + +test.register_message_test( + "Thermostat running mode reports 1 auto are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.ThermostatRunningMode:build_test_attr_report(mock_device, + 1):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode("auto")) + } + } +) + +test.register_message_test( + "FanControl fan mode reports 1 low are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 1):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.fanMode("low")) + } + } +) + +test.register_message_test( + "FanControl fan mode reports 2 medium are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 2):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.fanMode("medium")) + } + } +) + +test.register_message_test( + "FanControl fan mode reports 3 high are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 3):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.fanMode("high")) + } + } +) + +test.register_message_test( + "FanControl fan mode reports 5 auto are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 5):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.fanMode("auto")) + } + } +) + +test.register_message_test( + "Reported on status should be handled: on ep 3", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + true):from_endpoint(0x03) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("heat", capabilities.switch.switch.on()) + } + } +) + +test.register_message_test( + "Reported off status should be handled: off ep 3", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + false):from_endpoint(0x03) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("heat", capabilities.switch.switch.off()) + } + } +) + +test.register_message_test( + "Heating setpoint reports heatingSetpoint are handled : ep3", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.OccupiedHeatingSetpoint:build_test_attr_report(mock_device, + 2500):from_endpoint(0x03) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("heat", capabilities.thermostatHeatingSetpoint.heatingSetpoint({ value = 25.0, unit = "C" })) + } + } +) + +test.register_message_test( + "Reported on status should be handled: on ep 2", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + true):from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("fan", capabilities.switch.switch.on()) + } + } +) + +test.register_message_test( + "Reported off status should be handled: off ep 2", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + false):from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("fan", capabilities.switch.switch.off()) + } + } +) + +test.register_message_test( + "FanControl fan mode reports are handled : ep2", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 1):from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("fan", capabilities.fanMode.fanMode("low")) + } + } +) + +test.register_message_test( + "FanControl fan mode reports are handled : ep2", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 2):from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("fan", capabilities.fanMode.fanMode("medium")) + } + } +) + +test.register_message_test( + "FanControl fan mode reports are handled : ep2", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 3):from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("fan", capabilities.fanMode.fanMode("high")) + } + } +) + +test.register_message_test( + "FanControl fan mode reports 5 auto are handled : ep2", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 5):from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("fan", capabilities.fanMode.fanMode("auto")) + } + } +) + +test.register_message_test( + "Capability on command switch on should be handled : ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "main", command = "on", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.On(mock_device):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability off command switch off should be handled : ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "main", command = "off", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.Off(mock_device):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability on command switch on should be handled : ep3", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "heat", command = "on", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.On(mock_device):to_endpoint(0x03) } + } + } +) + +test.register_message_test( + "Capability off command switch off should be handled : ep3", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "heat", command = "off", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.Off(mock_device):to_endpoint(0x03) } + } + } +) + +test.register_message_test( + "Capability on command switch on should be handled : ep2", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "fan", command = "on", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.On(mock_device):to_endpoint(0x02) } + } + } +) + +test.register_message_test( + "Capability off command switch off should be handled : ep2", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "fan", command = "off", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.Off(mock_device):to_endpoint(0x02) } + } + } +) + +test.register_message_test( + "Capability temperatureSetpoint command setpoint 27 ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "temperatureSetpoint", component = "main", command = "setTemperatureSetpoint", args = { 27 } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device, 2700):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability thermostat command cool ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "thermostatMode", component = "main", command = "setThermostatMode", args = {"cool" } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, Thermostat.attributes.SystemMode:write(mock_device, 3):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability thermostat command dryair ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "thermostatMode", component = "main", command = "setThermostatMode", args = {"dryair" } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, Thermostat.attributes.SystemMode:write(mock_device, 8):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability thermostat command fanonly ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "thermostatMode", component = "main", command = "setThermostatMode", args = {"fanonly" } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, Thermostat.attributes.SystemMode:write(mock_device, 7):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability thermostat command heat ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "thermostatMode", component = "main", command = "setThermostatMode", args = { "heat"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, Thermostat.attributes.SystemMode:write(mock_device, 4):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability fanMode command auto ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "main", command = "setFanMode", args = {"auto"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 5):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability fanMode command low ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "main", command = "setFanMode", args = {"low"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 1):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability fanMode command medium ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "main", command = "setFanMode", args = {"medium"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 2):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability fanMode command high ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "main", command = "setFanMode" , args = {"high"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 3):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability thermostatHeatingSetpoint command setHeatingSetpoint 27 ep3", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "thermostatHeatingSetpoint", component = "heat", command = "setHeatingSetpoint", args = { 27 } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 2700):to_endpoint(0x03) } + } + } +) + +test.register_message_test( + "Capability fanMode command auto ep2", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "fan", command = "setFanMode", args = {"auto"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 5):to_endpoint(0x02) } + } + } +) + +test.register_message_test( + "Capability fanMode command low ep3", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "fan", command = "setFanMode", args = {"low"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 1):to_endpoint(0x02) } + } + } +) + +test.register_message_test( + "Capability fanMode command medium ep2", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "fan", command = "setFanMode", args = {"medium"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 2):to_endpoint(0x02) } + } + } +) + +test.register_message_test( + "Capability fanMode command high ep2", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "fan", command = "setFanMode" , args = {"high"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 3):to_endpoint(0x02) } + } + } +) + +test.register_coroutine_test( + "RecallScene command should be handled", + function() + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 4) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button1", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 5) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button2", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 6) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button3", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 7) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button4", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 8) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button5", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 9) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button6", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 10) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button7", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 11) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button8", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 12) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button9", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 13) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button10", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 14) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button11", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 15) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button12", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 16) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button13", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 17) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button14", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 18) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button15", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 19) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button16", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 20) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button17", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 21) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button18", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 22) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button19", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 23) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button20", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 24) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button21", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 25) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button22", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 26) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button23", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 27) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button24", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 28) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button25", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 29) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button26", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 30) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button27", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 31) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button28", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 32) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button29", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 33) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button30", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 34) }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-thermostat/src/wallhero/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/wallhero/can_handle.lua new file mode 100644 index 0000000000..476deedf2b --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/wallhero/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local FINGERPRINTS = require "wallhero.fingerprints" + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("wallhero") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-thermostat/src/wallhero/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/wallhero/fingerprints.lua new file mode 100644 index 0000000000..d1a79015f1 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/wallhero/fingerprints.lua @@ -0,0 +1,6 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "WALL HERO", model = "ACL-403STC1" } +} diff --git a/drivers/SmartThings/zigbee-thermostat/src/wallhero/init.lua b/drivers/SmartThings/zigbee-thermostat/src/wallhero/init.lua new file mode 100644 index 0000000000..1bf551ba72 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/wallhero/init.lua @@ -0,0 +1,233 @@ +-- Copyright 2026 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 capabilities = require "st.capabilities" +local log = require "log" +local socket = require "cosock.socket" +local zcl_clusters = require "st.zigbee.zcl.clusters" + +local Scenes = zcl_clusters.Scenes +local OnOff = zcl_clusters.OnOff +local FanControl = zcl_clusters.FanControl +local Thermostat = zcl_clusters.Thermostat +local ThermostatMode = capabilities.thermostatMode +local FanMode = capabilities.fanMode + +local function scenes_cluster_handler(driver, device, zb_rx) + local additional_fields = { + state_change = true + } + + local ep = zb_rx.address_header.src_endpoint.value-3 + local button_name = "button" .. ep + local event = capabilities.button.button.pushed(additional_fields) + local comp = device.profile.components[button_name] + if comp ~= nil then + device:emit_component_event(comp, event) + else + log.warn("Attempted to emit button event for unknown button: " .. button_name) + end +end + +local function on_off_attr_handler(driver, device, value, zb_rx) + local attr = capabilities.switch.switch + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, value.value and attr.on() or attr.off()) +end + +local function on_handler(driver, device, command) + device:send_to_component(command.component, OnOff.server.commands.On(device)) +end + +local function off_handler(driver, device, command) + device:send_to_component(command.component, OnOff.server.commands.Off(device)) +end + +local function caps_temperatureSetpoint_handler(driver, device, command) + local temperature = tonumber(command.args.setpoint)*100 + device:send_to_component(command.component, Thermostat.attributes.OccupiedCoolingSetpoint:write(device,temperature)) +end + +local SUPPORTED_FAN_MODES = { + { "auto", "high", "medium", "low"}, +} + +local FAN_MODE_TO_ZIGBEE = { + ["auto"] = 0x05, + ["low"] = 0x01, + ["medium"] = 0x02, + ["high"] = 0x03 +} + +local ZIGBEE_TO_FAN_MODES = { + [1] = "low" , + [2] = "medium" , + [3] = "high" , + [5] = "auto" +} + +local function fan_mode_attr_handler(driver, device, value, zb_rx) + local ep = zb_rx.address_header.src_endpoint.value + local str = ZIGBEE_TO_FAN_MODES[value.value] + if ep == 1 then + device:emit_component_event(device.profile.components.main,FanMode.fanMode({ value = str })) + elseif ep == 2 then + device:emit_component_event(device.profile.components.fan,FanMode.fanMode({ value = str })) + end +end + +local function setFanMode_handler(driver, device, command) + local value = FAN_MODE_TO_ZIGBEE[command.args.fanMode] + device:send_to_component(command.component, FanControl.attributes.FanMode:write(device, value)) +end + +local function thermostat_attr_occupiedCoolingSetpoint_handler(driver, device, value, zb_rx) + local ep = zb_rx.address_header.src_endpoint.value + if ep == 1 then + local temp = value.value/100 + device:emit_component_event(device.profile.components.main,capabilities.temperatureSetpoint.temperatureSetpoint({value = temp, unit = "C"})) + end +end + +local function do_refresh(driver, device) + device:send_to_component("main", OnOff.attributes.OnOff:read(device)) + device:send_to_component("main", Thermostat.attributes.LocalTemperature:read(device)) + device:send_to_component("main", Thermostat.attributes.OccupiedCoolingSetpoint:read(device)) + socket.sleep(1)--Avoid wireless congestion and packet loss + device:send_to_component("main", Thermostat.attributes.SystemMode:read(device)) + device:send_to_component("main", FanControl.attributes.FanMode:read(device)) + socket.sleep(1) + device:send_to_component("fan", OnOff.attributes.OnOff:read(device)) + device:send_to_component("fan", FanControl.attributes.FanMode:read(device)) + socket.sleep(1) + device:send_to_component("heat", OnOff.attributes.OnOff:read(device)) + device:send_to_component("heat", Thermostat.attributes.OccupiedHeatingSetpoint:read(device)) +end + +local function added_handler(driver, device) + device:emit_component_event(device.profile.components.main,ThermostatMode.supportedThermostatModes({"cool", "dryair", "fanonly", "heat"}, { visibility = { displayed = false } })) + device:emit_component_event(device.profile.components.main,FanMode.supportedFanModes( SUPPORTED_FAN_MODES[1] , { visibility = { displayed = false }})) + device:emit_component_event(device.profile.components.fan,FanMode.supportedFanModes( SUPPORTED_FAN_MODES[1] , { visibility = { displayed = false }})) + + device:emit_component_event(device.profile.components.main,capabilities.temperatureSetpoint.temperatureSetpointRange({ value = { minimum = 16.00, maximum = 32.00 }, unit = "C" })) + device:emit_component_event(device.profile.components.heat,capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = 16.00, maximum = 32.00 }, unit = "C" })) + + device:emit_component_event(device.profile.components.main,capabilities.switch.switch.off()) + device:emit_component_event(device.profile.components.heat,capabilities.switch.switch.off()) + device:emit_component_event(device.profile.components.fan,capabilities.switch.switch.off()) + + device:emit_component_event(device.profile.components.main,ThermostatMode.thermostatMode.cool()) + device:emit_component_event(device.profile.components.main,FanMode.fanMode.auto()) + device:emit_component_event(device.profile.components.fan,FanMode.fanMode.auto()) + device:emit_component_event(device.profile.components.main,capabilities.temperatureSetpoint.temperatureSetpoint({value = 26, unit = "C"})) + device:emit_component_event(device.profile.components.heat,capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 26, unit = "C"})) + + for _, component in pairs(device.profile.components) do + if component.id ~= "main" and component.id ~= "heat" and component.id ~= "fan" then + device:emit_component_event(component, + capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } })) + device:emit_component_event(component, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } })) + -- Without this time delay, the state of some buttons cannot be updated + socket.sleep(1) + end + end + do_refresh(driver, device) +end + +local function component_to_endpoint(device, component_id) + local ep_num + if component_id == "main" then + ep_num = 1 + elseif component_id == "fan" then + ep_num = 2 + elseif component_id == "heat" then + ep_num = 3 + end + return ep_num or device.fingerprinted_endpoint_id +end + +local function endpoint_to_component(device, ep) + if ep > 3 then + ep = ep - 3 + local button_comp = string.format("button%d+", ep) + if device.profile.components[button_comp] ~= nil then + return button_comp + else + return "button1" + end + else + if ep == 1 then + return "main" + elseif ep == 2 then + return "fan" + else + return "heat" + end + end +end + +local device_init = function(self, device) + device:set_component_to_endpoint_fn(component_to_endpoint) + device:set_endpoint_to_component_fn(endpoint_to_component) +end + +local wallhero_thermostat_3in1 = { + NAME = "Wall Hero thermostat 3in1", + supported_capabilities = { + capabilities.switch, + capabilities.temperatureSetpoint + }, + lifecycle_handlers = { + init = device_init, + added = added_handler + }, + health_check = false, + zigbee_handlers = { + cluster = { + [Scenes.ID] = { + [Scenes.server.commands.RecallScene.ID] = scenes_cluster_handler, + } + }, + attr = { + [OnOff.ID] = { + [OnOff.attributes.OnOff.ID] = on_off_attr_handler + }, + [FanControl.ID] = { + [FanControl.attributes.FanMode.ID] = fan_mode_attr_handler + }, + [Thermostat.ID] = { + [Thermostat.attributes.OccupiedCoolingSetpoint.ID] = thermostat_attr_occupiedCoolingSetpoint_handler + } + } + }, + capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = on_handler, + [capabilities.switch.commands.off.NAME] = off_handler + }, + [capabilities.temperatureSetpoint.ID] = { + [capabilities.temperatureSetpoint.commands.setTemperatureSetpoint.NAME] = caps_temperatureSetpoint_handler + }, + [capabilities.fanMode.ID] = { + [capabilities.fanMode.commands.setFanMode.NAME] = setFanMode_handler + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + } + }, + + can_handle = require("wallhero.can_handle") +} + +return wallhero_thermostat_3in1 diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index a67fdfeddf..fb80becab7 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -140,3 +140,4 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smart button MIR-SO100",麦乐克智能按钮MIR-SO100 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 +"WALLHERO Thermostat ACL-403STC1",墙壁侠温控ACL-403STC1