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, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/away_mode", "th1",
79 assertChannel(component, Climate.CURRENT_TEMPERATURE_CH_ID, "zigbee2mqtt/th1", "", "th1", NumberValue.class);
80 assertChannel(component, Climate.HOLD_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/preset", "th1",
82 assertChannel(component, Climate.MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/system_mode", "th1",
84 assertChannel(component, Climate.TEMPERATURE_CH_ID, "zigbee2mqtt/th1",
85 "zigbee2mqtt/th1/set/current_heating_setpoint", "th1", NumberValue.class);
87 publishMessage("zigbee2mqtt/th1", """
88 {"running_state": "idle", "away_mode": "ON", \
89 "local_temperature": "22.2", "preset": "schedule", "system_mode": "heat", \
90 "current_heating_setpoint": "24"}\
92 assertState(component, Climate.ACTION_CH_ID, new StringType("off"));
93 assertState(component, Climate.AWAY_MODE_CH_ID, OnOffType.ON);
94 assertState(component, Climate.CURRENT_TEMPERATURE_CH_ID, new QuantityType<>(22.2, SIUnits.CELSIUS));
95 assertState(component, Climate.HOLD_CH_ID, new StringType("schedule"));
96 assertState(component, Climate.MODE_CH_ID, new StringType("heat"));
97 assertState(component, Climate.TEMPERATURE_CH_ID, new QuantityType<>(24, SIUnits.CELSIUS));
99 component.getChannel(Climate.AWAY_MODE_CH_ID).getState().publishValue(OnOffType.OFF);
100 assertPublished("zigbee2mqtt/th1/set/away_mode", "OFF");
101 component.getChannel(Climate.HOLD_CH_ID).getState().publishValue(new StringType("eco"));
102 assertPublished("zigbee2mqtt/th1/set/preset", "eco");
103 component.getChannel(Climate.MODE_CH_ID).getState().publishValue(new StringType("auto"));
104 assertPublished("zigbee2mqtt/th1/set/system_mode", "auto");
105 component.getChannel(Climate.TEMPERATURE_CH_ID).getState().publishValue(new DecimalType(25));
106 assertPublished("zigbee2mqtt/th1/set/current_heating_setpoint", "25");
109 @SuppressWarnings("null")
111 public void testTS0601ClimateNotSendIfOff() {
112 var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
115 "action_template": "{% set values = {'idle':'off','heat':'heating','cool':'cooling','fan only':'fan'} %}{{ values[value_json.running_state] }}",\
116 "action_topic": "zigbee2mqtt/th1", "availability": [ {\
117 "topic": "zigbee2mqtt/bridge/state" } ],\
118 "away_mode_command_topic": "zigbee2mqtt/th1/set/away_mode",\
119 "away_mode_state_template": "{{ value_json.away_mode }}",\
120 "away_mode_state_topic": "zigbee2mqtt/th1",\
121 "current_temperature_template": "{{ value_json.local_temperature }}",\
122 "current_temperature_topic": "zigbee2mqtt/th1", "device": {\
123 "identifiers": [ "zigbee2mqtt_0x847127fffe11dd6a" ], "manufacturer": "TuYa",\
124 "model": "Radiator valve with thermostat (TS0601_thermostat)",\
125 "name": "th1", "sw_version": "Zigbee2MQTT 1.18.2" },\
126 "hold_command_topic": "zigbee2mqtt/th1/set/preset", "hold_modes": [\
127 "schedule", "manual", "boost", "complex", "comfort", "eco" ],\
128 "hold_state_template": "{{ value_json.preset }}",\
129 "hold_state_topic": "zigbee2mqtt/th1",\
130 "json_attributes_topic": "zigbee2mqtt/th1", "max_temp": "35",\
131 "min_temp": "5", "mode_command_topic": "zigbee2mqtt/th1/set/system_mode",\
132 "mode_state_template": "{{ value_json.system_mode }}",\
133 "mode_state_topic": "zigbee2mqtt/th1", "modes": [ "heat",\
134 "auto", "off" ], "name": "th1", "temp_step": 0.5,\
135 "temperature_command_topic": "zigbee2mqtt/th1/set/current_heating_setpoint",\
136 "temperature_state_template": "{{ value_json.current_heating_setpoint }}",\
137 "temperature_state_topic": "zigbee2mqtt/th1", "temperature_unit": "C",\
138 "power_command_topic": "zigbee2mqtt/th1/power",\
139 "unique_id": "0x847127fffe11dd6a_climate_zigbee2mqtt", "send_if_off": "false"}\
142 assertThat(component.channels.size(), is(7));
143 assertThat(component.getName(), is("th1"));
145 assertChannel(component, Climate.ACTION_CH_ID, "zigbee2mqtt/th1", "", "th1", TextValue.class);
146 assertChannel(component, Climate.AWAY_MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/away_mode", "th1",
148 assertChannel(component, Climate.CURRENT_TEMPERATURE_CH_ID, "zigbee2mqtt/th1", "", "th1", NumberValue.class);
149 assertChannel(component, Climate.HOLD_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/preset", "th1",
151 assertChannel(component, Climate.MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/system_mode", "th1",
153 assertChannel(component, Climate.TEMPERATURE_CH_ID, "zigbee2mqtt/th1",
154 "zigbee2mqtt/th1/set/current_heating_setpoint", "th1", NumberValue.class);
156 publishMessage("zigbee2mqtt/th1", """
157 {"running_state": "idle", "away_mode": "ON", \
158 "local_temperature": "22.2", "preset": "schedule", "system_mode": "heat", \
159 "current_heating_setpoint": "24"}\
161 assertState(component, Climate.ACTION_CH_ID, new StringType("off"));
162 assertState(component, Climate.AWAY_MODE_CH_ID, OnOffType.ON);
163 assertState(component, Climate.CURRENT_TEMPERATURE_CH_ID, new QuantityType<>(22.2, SIUnits.CELSIUS));
164 assertState(component, Climate.HOLD_CH_ID, new StringType("schedule"));
165 assertState(component, Climate.MODE_CH_ID, new StringType("heat"));
166 assertState(component, Climate.TEMPERATURE_CH_ID, new QuantityType<>(24, SIUnits.CELSIUS));
168 // Climate is in OFF state
169 component.getChannel(Climate.AWAY_MODE_CH_ID).getState().publishValue(OnOffType.OFF);
170 assertNotPublished("zigbee2mqtt/th1/set/away_mode", "OFF");
171 component.getChannel(Climate.HOLD_CH_ID).getState().publishValue(new StringType("eco"));
172 assertNotPublished("zigbee2mqtt/th1/set/preset", "eco");
173 component.getChannel(Climate.MODE_CH_ID).getState().publishValue(new StringType("auto"));
174 assertNotPublished("zigbee2mqtt/th1/set/system_mode", "auto");
175 component.getChannel(Climate.TEMPERATURE_CH_ID).getState().publishValue(new DecimalType(25));
176 assertNotPublished("zigbee2mqtt/th1/set/current_heating_setpoint", "25");
177 component.getChannel(Climate.POWER_CH_ID).getState().publishValue(OnOffType.ON);
178 assertPublished("zigbee2mqtt/th1/power", "ON");
181 publishMessage("zigbee2mqtt/th1", """
182 {"running_state": "heat", "away_mode": "ON", \
183 "local_temperature": "22.2", "preset": "schedule", "system_mode": "heat", \
184 "current_heating_setpoint": "24"}\
187 // Climate is in ON state
188 component.getChannel(Climate.AWAY_MODE_CH_ID).getState().publishValue(OnOffType.OFF);
189 assertPublished("zigbee2mqtt/th1/set/away_mode", "OFF");
190 component.getChannel(Climate.HOLD_CH_ID).getState().publishValue(new StringType("eco"));
191 assertPublished("zigbee2mqtt/th1/set/preset", "eco");
192 component.getChannel(Climate.MODE_CH_ID).getState().publishValue(new StringType("auto"));
193 assertPublished("zigbee2mqtt/th1/set/system_mode", "auto");
194 component.getChannel(Climate.TEMPERATURE_CH_ID).getState().publishValue(new DecimalType(25));
195 assertPublished("zigbee2mqtt/th1/set/current_heating_setpoint", "25");
198 @SuppressWarnings("null")
200 public void testClimate() {
201 var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
202 {"action_template": "{{ value_json.action }}", "action_topic": "zigbee2mqtt/th1",\
203 "aux_command_topic": "zigbee2mqtt/th1/aux",\
204 "aux_state_template": "{{ value_json.aux }}", "aux_state_topic": "zigbee2mqtt/th1",\
205 "away_mode_command_topic": "zigbee2mqtt/th1/away_mode",\
206 "away_mode_state_template": "{{ value_json.away_mode }}",\
207 "away_mode_state_topic": "zigbee2mqtt/th1",\
208 "current_temperature_template": "{{ value_json.current_temperature }}",\
209 "current_temperature_topic": "zigbee2mqtt/th1",\
210 "fan_mode_command_template": "fan_mode={{ value }}",\
211 "fan_mode_command_topic": "zigbee2mqtt/th1/fan_mode",\
212 "fan_mode_state_template": "{{ value_json.fan_mode }}",\
213 "fan_mode_state_topic": "zigbee2mqtt/th1", "fan_modes": [ "p1",\
214 "p2" ], "hold_command_template": "hold={{ value }}",\
215 "hold_command_topic": "zigbee2mqtt/th1/hold",\
216 "hold_state_template": "{{ value_json.hold }}",\
217 "hold_state_topic": "zigbee2mqtt/th1", "hold_modes": [ "u1", "u2",\
218 "u3" ], "json_attributes_template": "{{ value_json.attrs }}",\
219 "json_attributes_topic": "zigbee2mqtt/th1",\
220 "mode_command_template": "mode={{ value }}",\
221 "mode_command_topic": "zigbee2mqtt/th1/mode",\
222 "mode_state_template": "{{ value_json.mode }}",\
223 "mode_state_topic": "zigbee2mqtt/th1", "modes": [ "B1", "B2"\
224 ], "swing_command_template": "swing={{ value }}",\
225 "swing_command_topic": "zigbee2mqtt/th1/swing",\
226 "swing_state_template": "{{ value_json.swing }}",\
227 "swing_state_topic": "zigbee2mqtt/th1", "swing_modes": [ "G1",\
228 "G2" ], "temperature_command_template": "temperature={{ value }}",\
229 "temperature_command_topic": "zigbee2mqtt/th1/temperature",\
230 "temperature_state_template": "{{ value_json.temperature }}",\
231 "temperature_state_topic": "zigbee2mqtt/th1",\
232 "temperature_high_command_template": "temperature_high={{ value }}",\
233 "temperature_high_command_topic": "zigbee2mqtt/th1/temperature_high",\
234 "temperature_high_state_template": "{{ value_json.temperature_high }}",\
235 "temperature_high_state_topic": "zigbee2mqtt/th1",\
236 "temperature_low_command_template": "temperature_low={{ value }}",\
237 "temperature_low_command_topic": "zigbee2mqtt/th1/temperature_low",\
238 "temperature_low_state_template": "{{ value_json.temperature_low }}",\
239 "temperature_low_state_topic": "zigbee2mqtt/th1",\
240 "power_command_topic": "zigbee2mqtt/th1/power", "initial": "10",\
241 "max_temp": "40", "min_temp": "0", "temperature_unit": "F",\
242 "temp_step": "1", "precision": "0.5", "send_if_off": "false" }\
245 assertThat(component.channels.size(), is(12));
246 assertThat(component.getName(), is("MQTT HVAC"));
248 assertChannel(component, Climate.ACTION_CH_ID, "zigbee2mqtt/th1", "", "MQTT HVAC", TextValue.class);
249 assertChannel(component, Climate.AUX_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/aux", "MQTT HVAC",
251 assertChannel(component, Climate.AWAY_MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/away_mode", "MQTT HVAC",
253 assertChannel(component, Climate.CURRENT_TEMPERATURE_CH_ID, "zigbee2mqtt/th1", "", "MQTT HVAC",
255 assertChannel(component, Climate.FAN_MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/fan_mode", "MQTT HVAC",
257 assertChannel(component, Climate.HOLD_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/hold", "MQTT HVAC",
259 assertChannel(component, Climate.MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/mode", "MQTT HVAC",
261 assertChannel(component, Climate.SWING_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/swing", "MQTT HVAC",
263 assertChannel(component, Climate.TEMPERATURE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/temperature",
264 "MQTT HVAC", NumberValue.class);
265 assertChannel(component, Climate.TEMPERATURE_HIGH_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/temperature_high",
266 "MQTT HVAC", NumberValue.class);
267 assertChannel(component, Climate.TEMPERATURE_LOW_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/temperature_low",
268 "MQTT HVAC", NumberValue.class);
269 assertChannel(component, Climate.POWER_CH_ID, "", "zigbee2mqtt/th1/power", "MQTT HVAC", OnOffValue.class);
271 publishMessage("zigbee2mqtt/th1", """
272 { "action": "fan", "aux": "ON", "away_mode": "OFF", \
273 "current_temperature": "35.5", "fan_mode": "p2", "hold": "u2", \
274 "mode": "B1", "swing": "G1", "temperature": "30", \
275 "temperature_high": "37", "temperature_low": "20" }\
278 assertState(component, Climate.ACTION_CH_ID, new StringType("fan"));
279 assertState(component, Climate.AUX_CH_ID, OnOffType.ON);
280 assertState(component, Climate.AWAY_MODE_CH_ID, OnOffType.OFF);
281 assertState(component, Climate.CURRENT_TEMPERATURE_CH_ID, new QuantityType<>(35.5, ImperialUnits.FAHRENHEIT));
282 assertState(component, Climate.FAN_MODE_CH_ID, new StringType("p2"));
283 assertState(component, Climate.HOLD_CH_ID, new StringType("u2"));
284 assertState(component, Climate.MODE_CH_ID, new StringType("B1"));
285 assertState(component, Climate.SWING_CH_ID, new StringType("G1"));
286 assertState(component, Climate.TEMPERATURE_CH_ID, new QuantityType<>(30, ImperialUnits.FAHRENHEIT));
287 assertState(component, Climate.TEMPERATURE_HIGH_CH_ID, new QuantityType<>(37, ImperialUnits.FAHRENHEIT));
288 assertState(component, Climate.TEMPERATURE_LOW_CH_ID, new QuantityType<>(20, ImperialUnits.FAHRENHEIT));
290 component.getChannel(Climate.AUX_CH_ID).getState().publishValue(OnOffType.OFF);
291 assertPublished("zigbee2mqtt/th1/aux", "OFF");
292 component.getChannel(Climate.AWAY_MODE_CH_ID).getState().publishValue(OnOffType.ON);
293 assertPublished("zigbee2mqtt/th1/away_mode", "ON");
294 component.getChannel(Climate.FAN_MODE_CH_ID).getState().publishValue(new StringType("p1"));
295 assertPublished("zigbee2mqtt/th1/fan_mode", "fan_mode=p1");
296 component.getChannel(Climate.HOLD_CH_ID).getState().publishValue(new StringType("u3"));
297 assertPublished("zigbee2mqtt/th1/hold", "hold=u3");
298 component.getChannel(Climate.MODE_CH_ID).getState().publishValue(new StringType("B2"));
299 assertPublished("zigbee2mqtt/th1/mode", "mode=B2");
300 component.getChannel(Climate.SWING_CH_ID).getState().publishValue(new StringType("G2"));
301 assertPublished("zigbee2mqtt/th1/swing", "swing=G2");
302 component.getChannel(Climate.TEMPERATURE_CH_ID).getState().publishValue(new DecimalType(30.5));
303 assertPublished("zigbee2mqtt/th1/temperature", "temperature=30.5");
304 component.getChannel(Climate.TEMPERATURE_HIGH_CH_ID).getState().publishValue(new DecimalType(39.5));
305 assertPublished("zigbee2mqtt/th1/temperature_high", "temperature_high=39.5");
306 component.getChannel(Climate.TEMPERATURE_LOW_CH_ID).getState().publishValue(new DecimalType(19.5));
307 assertPublished("zigbee2mqtt/th1/temperature_low", "temperature_low=19.5");
308 component.getChannel(Climate.POWER_CH_ID).getState().publishValue(OnOffType.OFF);
309 assertPublished("zigbee2mqtt/th1/power", "OFF");
313 protected Set<String> getConfigTopics() {
314 return Set.of(CONFIG_TOPIC);