2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mqtt.homeassistant.internal.component;
15 import static org.hamcrest.CoreMatchers.is;
16 import static org.hamcrest.MatcherAssert.assertThat;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.junit.jupiter.api.Test;
22 import org.openhab.binding.mqtt.generic.values.NumberValue;
23 import org.openhab.binding.mqtt.generic.values.OnOffValue;
24 import org.openhab.binding.mqtt.generic.values.TextValue;
25 import org.openhab.core.library.types.DecimalType;
26 import org.openhab.core.library.types.OnOffType;
27 import org.openhab.core.library.types.QuantityType;
28 import org.openhab.core.library.types.StringType;
29 import org.openhab.core.library.unit.ImperialUnits;
30 import org.openhab.core.library.unit.SIUnits;
33 * Tests for {@link Climate}
35 * @author Anton Kharuzhy - Initial contribution
38 public class ClimateTests extends AbstractComponentTests {
39 public static final String CONFIG_TOPIC = "climate/0x847127fffe11dd6a_climate_zigbee2mqtt";
41 @SuppressWarnings("null")
43 public void testTS0601Climate() {
44 var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
47 "action_template": "{% set values = {'idle':'off','heat':'heating','cool':'cooling','fan only':'fan'} %}{{ values[value_json.running_state] }}",\
48 "action_topic": "zigbee2mqtt/th1", "availability": [ {\
49 "topic": "zigbee2mqtt/bridge/state" } ],\
50 "away_mode_command_topic": "zigbee2mqtt/th1/set/away_mode",\
51 "away_mode_state_template": "{{ value_json.away_mode }}",\
52 "away_mode_state_topic": "zigbee2mqtt/th1",\
53 "current_temperature_template": "{{ value_json.local_temperature }}",\
54 "current_temperature_topic": "zigbee2mqtt/th1", "device": {\
55 "identifiers": [ "zigbee2mqtt_0x847127fffe11dd6a" ], "manufacturer": "TuYa",\
56 "model": "Radiator valve with thermostat (TS0601_thermostat)",\
57 "name": "th1", "sw_version": "Zigbee2MQTT 1.18.2" },\
58 "hold_command_topic": "zigbee2mqtt/th1/set/preset", "hold_modes": [\
59 "schedule", "manual", "boost", "complex", "comfort", "eco" ],\
60 "hold_state_template": "{{ value_json.preset }}",\
61 "hold_state_topic": "zigbee2mqtt/th1",\
62 "json_attributes_topic": "zigbee2mqtt/th1", "max_temp": "35",\
63 "min_temp": "5", "mode_command_topic": "zigbee2mqtt/th1/set/system_mode",\
64 "mode_state_template": "{{ value_json.system_mode }}",\
65 "mode_state_topic": "zigbee2mqtt/th1", "modes": [ "heat",\
66 "auto", "off" ], "name": "th1", "temp_step": 0.5,\
67 "temperature_command_topic": "zigbee2mqtt/th1/set/current_heating_setpoint",\
68 "temperature_state_template": "{{ value_json.current_heating_setpoint }}",\
69 "temperature_state_topic": "zigbee2mqtt/th1", "temperature_unit": "C",\
70 "unique_id": "0x847127fffe11dd6a_climate_zigbee2mqtt"}\
73 assertThat(component.channels.size(), is(6));
74 assertThat(component.getName(), is("th1"));
76 assertChannel(component, Climate.ACTION_CH_ID, "zigbee2mqtt/th1", "", "th1", TextValue.class);
77 assertChannel(component, Climate.AWAY_MODE_CH_ID_DEPRECATED, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/away_mode",
78 "th1", OnOffValue.class);
79 assertChannel(component, Climate.CURRENT_TEMPERATURE_CH_ID_DEPRECATED, "zigbee2mqtt/th1", "", "th1",
81 assertChannel(component, Climate.HOLD_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/preset", "th1",
83 assertChannel(component, Climate.MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/system_mode", "th1",
85 assertChannel(component, Climate.TEMPERATURE_CH_ID, "zigbee2mqtt/th1",
86 "zigbee2mqtt/th1/set/current_heating_setpoint", "th1", NumberValue.class);
88 publishMessage("zigbee2mqtt/th1", """
89 {"running_state": "idle", "away_mode": "ON", \
90 "local_temperature": "22.2", "preset": "schedule", "system_mode": "heat", \
91 "current_heating_setpoint": "24"}\
93 assertState(component, Climate.ACTION_CH_ID, new StringType("off"));
94 assertState(component, Climate.AWAY_MODE_CH_ID_DEPRECATED, OnOffType.ON);
95 assertState(component, Climate.CURRENT_TEMPERATURE_CH_ID_DEPRECATED, new QuantityType<>(22.2, SIUnits.CELSIUS));
96 assertState(component, Climate.HOLD_CH_ID, new StringType("schedule"));
97 assertState(component, Climate.MODE_CH_ID, new StringType("heat"));
98 assertState(component, Climate.TEMPERATURE_CH_ID, new QuantityType<>(24, SIUnits.CELSIUS));
100 component.getChannel(Climate.AWAY_MODE_CH_ID_DEPRECATED).getState().publishValue(OnOffType.OFF);
101 assertPublished("zigbee2mqtt/th1/set/away_mode", "OFF");
102 component.getChannel(Climate.HOLD_CH_ID).getState().publishValue(new StringType("eco"));
103 assertPublished("zigbee2mqtt/th1/set/preset", "eco");
104 component.getChannel(Climate.MODE_CH_ID).getState().publishValue(new StringType("auto"));
105 assertPublished("zigbee2mqtt/th1/set/system_mode", "auto");
106 component.getChannel(Climate.TEMPERATURE_CH_ID).getState().publishValue(new DecimalType(25));
107 assertPublished("zigbee2mqtt/th1/set/current_heating_setpoint", "25");
110 @SuppressWarnings("null")
112 public void testTS0601ClimateNotSendIfOff() {
113 var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
116 "action_template": "{% set values = {'idle':'off','heat':'heating','cool':'cooling','fan only':'fan'} %}{{ values[value_json.running_state] }}",\
117 "action_topic": "zigbee2mqtt/th1", "availability": [ {\
118 "topic": "zigbee2mqtt/bridge/state" } ],\
119 "away_mode_command_topic": "zigbee2mqtt/th1/set/away_mode",\
120 "away_mode_state_template": "{{ value_json.away_mode }}",\
121 "away_mode_state_topic": "zigbee2mqtt/th1",\
122 "current_temperature_template": "{{ value_json.local_temperature }}",\
123 "current_temperature_topic": "zigbee2mqtt/th1", "device": {\
124 "identifiers": [ "zigbee2mqtt_0x847127fffe11dd6a" ], "manufacturer": "TuYa",\
125 "model": "Radiator valve with thermostat (TS0601_thermostat)",\
126 "name": "th1", "sw_version": "Zigbee2MQTT 1.18.2" },\
127 "hold_command_topic": "zigbee2mqtt/th1/set/preset", "hold_modes": [\
128 "schedule", "manual", "boost", "complex", "comfort", "eco" ],\
129 "hold_state_template": "{{ value_json.preset }}",\
130 "hold_state_topic": "zigbee2mqtt/th1",\
131 "json_attributes_topic": "zigbee2mqtt/th1", "max_temp": "35",\
132 "min_temp": "5", "mode_command_topic": "zigbee2mqtt/th1/set/system_mode",\
133 "mode_state_template": "{{ value_json.system_mode }}",\
134 "mode_state_topic": "zigbee2mqtt/th1", "modes": [ "heat",\
135 "auto", "off" ], "name": "th1", "temp_step": 0.5,\
136 "temperature_command_topic": "zigbee2mqtt/th1/set/current_heating_setpoint",\
137 "temperature_state_template": "{{ value_json.current_heating_setpoint }}",\
138 "temperature_state_topic": "zigbee2mqtt/th1", "temperature_unit": "C",\
139 "power_command_topic": "zigbee2mqtt/th1/power",\
140 "unique_id": "0x847127fffe11dd6a_climate_zigbee2mqtt", "send_if_off": "false"}\
143 assertThat(component.channels.size(), is(7));
144 assertThat(component.getName(), is("th1"));
146 assertChannel(component, Climate.ACTION_CH_ID, "zigbee2mqtt/th1", "", "th1", TextValue.class);
147 assertChannel(component, Climate.AWAY_MODE_CH_ID_DEPRECATED, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/away_mode",
148 "th1", OnOffValue.class);
149 assertChannel(component, Climate.CURRENT_TEMPERATURE_CH_ID_DEPRECATED, "zigbee2mqtt/th1", "", "th1",
151 assertChannel(component, Climate.HOLD_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/preset", "th1",
153 assertChannel(component, Climate.MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/system_mode", "th1",
155 assertChannel(component, Climate.TEMPERATURE_CH_ID, "zigbee2mqtt/th1",
156 "zigbee2mqtt/th1/set/current_heating_setpoint", "th1", NumberValue.class);
158 publishMessage("zigbee2mqtt/th1", """
159 {"running_state": "idle", "away_mode": "ON", \
160 "local_temperature": "22.2", "preset": "schedule", "system_mode": "heat", \
161 "current_heating_setpoint": "24"}\
163 assertState(component, Climate.ACTION_CH_ID, new StringType("off"));
164 assertState(component, Climate.AWAY_MODE_CH_ID_DEPRECATED, OnOffType.ON);
165 assertState(component, Climate.CURRENT_TEMPERATURE_CH_ID_DEPRECATED, new QuantityType<>(22.2, SIUnits.CELSIUS));
166 assertState(component, Climate.HOLD_CH_ID, new StringType("schedule"));
167 assertState(component, Climate.MODE_CH_ID, new StringType("heat"));
168 assertState(component, Climate.TEMPERATURE_CH_ID, new QuantityType<>(24, SIUnits.CELSIUS));
170 // Climate is in OFF state
171 component.getChannel(Climate.AWAY_MODE_CH_ID_DEPRECATED).getState().publishValue(OnOffType.OFF);
172 assertNotPublished("zigbee2mqtt/th1/set/away_mode", "OFF");
173 component.getChannel(Climate.HOLD_CH_ID).getState().publishValue(new StringType("eco"));
174 assertNotPublished("zigbee2mqtt/th1/set/preset", "eco");
175 component.getChannel(Climate.MODE_CH_ID).getState().publishValue(new StringType("auto"));
176 assertNotPublished("zigbee2mqtt/th1/set/system_mode", "auto");
177 component.getChannel(Climate.TEMPERATURE_CH_ID).getState().publishValue(new DecimalType(25));
178 assertNotPublished("zigbee2mqtt/th1/set/current_heating_setpoint", "25");
179 component.getChannel(Climate.POWER_CH_ID).getState().publishValue(OnOffType.ON);
180 assertPublished("zigbee2mqtt/th1/power", "ON");
183 publishMessage("zigbee2mqtt/th1", """
184 {"running_state": "heat", "away_mode": "ON", \
185 "local_temperature": "22.2", "preset": "schedule", "system_mode": "heat", \
186 "current_heating_setpoint": "24"}\
189 // Climate is in ON state
190 component.getChannel(Climate.AWAY_MODE_CH_ID_DEPRECATED).getState().publishValue(OnOffType.OFF);
191 assertPublished("zigbee2mqtt/th1/set/away_mode", "OFF");
192 component.getChannel(Climate.HOLD_CH_ID).getState().publishValue(new StringType("eco"));
193 assertPublished("zigbee2mqtt/th1/set/preset", "eco");
194 component.getChannel(Climate.MODE_CH_ID).getState().publishValue(new StringType("auto"));
195 assertPublished("zigbee2mqtt/th1/set/system_mode", "auto");
196 component.getChannel(Climate.TEMPERATURE_CH_ID).getState().publishValue(new DecimalType(25));
197 assertPublished("zigbee2mqtt/th1/set/current_heating_setpoint", "25");
200 @SuppressWarnings("null")
202 public void testClimate() {
203 var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
204 {"action_template": "{{ value_json.action }}", "action_topic": "zigbee2mqtt/th1",\
205 "aux_command_topic": "zigbee2mqtt/th1/aux",\
206 "aux_state_template": "{{ value_json.aux }}", "aux_state_topic": "zigbee2mqtt/th1",\
207 "away_mode_command_topic": "zigbee2mqtt/th1/away_mode",\
208 "away_mode_state_template": "{{ value_json.away_mode }}",\
209 "away_mode_state_topic": "zigbee2mqtt/th1",\
210 "current_temperature_template": "{{ value_json.current_temperature }}",\
211 "current_temperature_topic": "zigbee2mqtt/th1",\
212 "fan_mode_command_template": "fan_mode={{ value }}",\
213 "fan_mode_command_topic": "zigbee2mqtt/th1/fan_mode",\
214 "fan_mode_state_template": "{{ value_json.fan_mode }}",\
215 "fan_mode_state_topic": "zigbee2mqtt/th1", "fan_modes": [ "p1",\
216 "p2" ], "hold_command_template": "hold={{ value }}",\
217 "hold_command_topic": "zigbee2mqtt/th1/hold",\
218 "hold_state_template": "{{ value_json.hold }}",\
219 "hold_state_topic": "zigbee2mqtt/th1", "hold_modes": [ "u1", "u2",\
220 "u3" ], "json_attributes_template": "{{ value_json.attrs }}",\
221 "json_attributes_topic": "zigbee2mqtt/th1",\
222 "mode_command_template": "mode={{ value }}",\
223 "mode_command_topic": "zigbee2mqtt/th1/mode",\
224 "mode_state_template": "{{ value_json.mode }}",\
225 "mode_state_topic": "zigbee2mqtt/th1", "modes": [ "B1", "B2"\
226 ], "swing_command_template": "swing={{ value }}",\
227 "swing_command_topic": "zigbee2mqtt/th1/swing",\
228 "swing_state_template": "{{ value_json.swing }}",\
229 "swing_state_topic": "zigbee2mqtt/th1", "swing_modes": [ "G1",\
230 "G2" ], "temperature_command_template": "temperature={{ value }}",\
231 "temperature_command_topic": "zigbee2mqtt/th1/temperature",\
232 "temperature_state_template": "{{ value_json.temperature }}",\
233 "temperature_state_topic": "zigbee2mqtt/th1",\
234 "temperature_high_command_template": "temperature_high={{ value }}",\
235 "temperature_high_command_topic": "zigbee2mqtt/th1/temperature_high",\
236 "temperature_high_state_template": "{{ value_json.temperature_high }}",\
237 "temperature_high_state_topic": "zigbee2mqtt/th1",\
238 "temperature_low_command_template": "temperature_low={{ value }}",\
239 "temperature_low_command_topic": "zigbee2mqtt/th1/temperature_low",\
240 "temperature_low_state_template": "{{ value_json.temperature_low }}",\
241 "temperature_low_state_topic": "zigbee2mqtt/th1",\
242 "power_command_topic": "zigbee2mqtt/th1/power", "initial": "10",\
243 "max_temp": "40", "min_temp": "0", "temperature_unit": "F",\
244 "temp_step": "1", "precision": "0.5", "send_if_off": "false" }\
247 assertThat(component.channels.size(), is(12));
248 assertThat(component.getName(), is("MQTT HVAC"));
250 assertChannel(component, Climate.ACTION_CH_ID, "zigbee2mqtt/th1", "", "MQTT HVAC", TextValue.class);
251 assertChannel(component, Climate.AUX_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/aux", "MQTT HVAC",
253 assertChannel(component, Climate.AWAY_MODE_CH_ID_DEPRECATED, "zigbee2mqtt/th1", "zigbee2mqtt/th1/away_mode",
254 "MQTT HVAC", OnOffValue.class);
255 assertChannel(component, Climate.CURRENT_TEMPERATURE_CH_ID_DEPRECATED, "zigbee2mqtt/th1", "", "MQTT HVAC",
257 assertChannel(component, Climate.FAN_MODE_CH_ID_DEPRECATED, "zigbee2mqtt/th1", "zigbee2mqtt/th1/fan_mode",
258 "MQTT HVAC", TextValue.class);
259 assertChannel(component, Climate.HOLD_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/hold", "MQTT HVAC",
261 assertChannel(component, Climate.MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/mode", "MQTT HVAC",
263 assertChannel(component, Climate.SWING_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/swing", "MQTT HVAC",
265 assertChannel(component, Climate.TEMPERATURE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/temperature",
266 "MQTT HVAC", NumberValue.class);
267 assertChannel(component, Climate.TEMPERATURE_HIGH_CH_ID_DEPRECATED, "zigbee2mqtt/th1",
268 "zigbee2mqtt/th1/temperature_high", "MQTT HVAC", NumberValue.class);
269 assertChannel(component, Climate.TEMPERATURE_LOW_CH_ID_DEPRECATED, "zigbee2mqtt/th1",
270 "zigbee2mqtt/th1/temperature_low", "MQTT HVAC", NumberValue.class);
271 assertChannel(component, Climate.POWER_CH_ID, "", "zigbee2mqtt/th1/power", "MQTT HVAC", OnOffValue.class);
273 publishMessage("zigbee2mqtt/th1", """
274 { "action": "fan", "aux": "ON", "away_mode": "OFF", \
275 "current_temperature": "35.5", "fan_mode": "p2", "hold": "u2", \
276 "mode": "B1", "swing": "G1", "temperature": "30", \
277 "temperature_high": "37", "temperature_low": "20" }\
280 assertState(component, Climate.ACTION_CH_ID, new StringType("fan"));
281 assertState(component, Climate.AUX_CH_ID, OnOffType.ON);
282 assertState(component, Climate.AWAY_MODE_CH_ID_DEPRECATED, OnOffType.OFF);
283 assertState(component, Climate.CURRENT_TEMPERATURE_CH_ID_DEPRECATED,
284 new QuantityType<>(35.5, ImperialUnits.FAHRENHEIT));
285 assertState(component, Climate.FAN_MODE_CH_ID_DEPRECATED, new StringType("p2"));
286 assertState(component, Climate.HOLD_CH_ID, new StringType("u2"));
287 assertState(component, Climate.MODE_CH_ID, new StringType("B1"));
288 assertState(component, Climate.SWING_CH_ID, new StringType("G1"));
289 assertState(component, Climate.TEMPERATURE_CH_ID, new QuantityType<>(30, ImperialUnits.FAHRENHEIT));
290 assertState(component, Climate.TEMPERATURE_HIGH_CH_ID_DEPRECATED,
291 new QuantityType<>(37, ImperialUnits.FAHRENHEIT));
292 assertState(component, Climate.TEMPERATURE_LOW_CH_ID_DEPRECATED,
293 new QuantityType<>(20, ImperialUnits.FAHRENHEIT));
295 component.getChannel(Climate.AUX_CH_ID).getState().publishValue(OnOffType.OFF);
296 assertPublished("zigbee2mqtt/th1/aux", "OFF");
297 component.getChannel(Climate.AWAY_MODE_CH_ID_DEPRECATED).getState().publishValue(OnOffType.ON);
298 assertPublished("zigbee2mqtt/th1/away_mode", "ON");
299 component.getChannel(Climate.FAN_MODE_CH_ID_DEPRECATED).getState().publishValue(new StringType("p1"));
300 assertPublished("zigbee2mqtt/th1/fan_mode", "fan_mode=p1");
301 component.getChannel(Climate.HOLD_CH_ID).getState().publishValue(new StringType("u3"));
302 assertPublished("zigbee2mqtt/th1/hold", "hold=u3");
303 component.getChannel(Climate.MODE_CH_ID).getState().publishValue(new StringType("B2"));
304 assertPublished("zigbee2mqtt/th1/mode", "mode=B2");
305 component.getChannel(Climate.SWING_CH_ID).getState().publishValue(new StringType("G2"));
306 assertPublished("zigbee2mqtt/th1/swing", "swing=G2");
307 component.getChannel(Climate.TEMPERATURE_CH_ID).getState().publishValue(new DecimalType(30.5));
308 assertPublished("zigbee2mqtt/th1/temperature", "temperature=30.5");
309 component.getChannel(Climate.TEMPERATURE_HIGH_CH_ID_DEPRECATED).getState().publishValue(new DecimalType(39.5));
310 assertPublished("zigbee2mqtt/th1/temperature_high", "temperature_high=39.5");
311 component.getChannel(Climate.TEMPERATURE_LOW_CH_ID_DEPRECATED).getState().publishValue(new DecimalType(19.5));
312 assertPublished("zigbee2mqtt/th1/temperature_low", "temperature_low=19.5");
313 component.getChannel(Climate.POWER_CH_ID).getState().publishValue(OnOffType.OFF);
314 assertPublished("zigbee2mqtt/th1/power", "OFF");
318 protected Set<String> getConfigTopics() {
319 return Set.of(CONFIG_TOPIC);