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.CoreMatchers.notNullValue;
17 import static org.hamcrest.CoreMatchers.nullValue;
18 import static org.hamcrest.MatcherAssert.assertThat;
20 import java.math.BigDecimal;
21 import java.math.MathContext;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.junit.jupiter.api.Test;
27 import org.openhab.binding.mqtt.generic.values.ColorValue;
28 import org.openhab.binding.mqtt.generic.values.OnOffValue;
29 import org.openhab.binding.mqtt.generic.values.PercentageValue;
30 import org.openhab.binding.mqtt.generic.values.TextValue;
31 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.HSBType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.PercentType;
36 import org.openhab.core.library.types.StringType;
39 * Tests for {@link Light} confirming to the default schema
41 * @author Anton Kharuzhy - Initial contribution
44 public class DefaultSchemaLightTests extends AbstractComponentTests {
45 public static final String CONFIG_TOPIC = "light/0x0000000000000000_light_zigbee2mqtt";
48 public void testRgb() throws InterruptedException {
50 var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
55 "topic": "zigbee2mqtt/bridge/state" \
60 "zigbee2mqtt_0x0000000000000000" \
62 "manufacturer": "Lights inc", \
63 "model": "light v1", \
65 "sw_version": "Zigbee2MQTT 1.18.2" \
68 "state_topic": "zigbee2mqtt/light/state", \
69 "command_topic": "zigbee2mqtt/light/set/state", \
70 "state_value_template": "{{ value_json.power }}", \
71 "payload_on": "ON_", \
72 "payload_off": "OFF_", \
73 "rgb_state_topic": "zigbee2mqtt/light/rgb", \
74 "rgb_command_topic": "zigbee2mqtt/light/set/rgb", \
75 "rgb_value_template": "{{ value_json.rgb }}", \
76 "brightness_state_topic": "zigbee2mqtt/light/brightness", \
77 "brightness_command_topic": "zigbee2mqtt/light/set/brightness", \
78 "brightness_value_template": "{{ value_json.br }}" \
83 assertThat(component.channels.size(), is(1));
84 assertThat(component.getName(), is("light"));
86 assertChannel(component, Light.COLOR_CHANNEL_ID, "", "dummy", "Color", ColorValue.class);
89 ComponentChannel onOffChannel = component.onOffChannel;
90 assertThat(onOffChannel, is(notNullValue()));
91 if (onOffChannel != null) {
92 assertChannel(onOffChannel, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state", "On/Off State",
96 ComponentChannel brightnessChannel = component.brightnessChannel;
97 assertThat(brightnessChannel, is(notNullValue()));
98 if (brightnessChannel != null) {
99 assertChannel(brightnessChannel, "zigbee2mqtt/light/brightness", "zigbee2mqtt/light/set/brightness",
100 "Brightness", PercentageValue.class);
103 publishMessage("zigbee2mqtt/light/state", "{\"power\": \"ON_\"}");
104 assertState(component, Light.COLOR_CHANNEL_ID, HSBType.WHITE);
105 publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"10,20,30\"}");
106 assertState(component, Light.COLOR_CHANNEL_ID, HSBType.fromRGB(10, 20, 30));
107 publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"255,255,255\"}");
108 assertState(component, Light.COLOR_CHANNEL_ID, HSBType.WHITE);
110 sendCommand(component, Light.COLOR_CHANNEL_ID, HSBType.BLUE);
111 assertPublished("zigbee2mqtt/light/set/rgb", "0,0,255");
113 // Brightness commands should route to the correct topic
114 sendCommand(component, Light.COLOR_CHANNEL_ID, new PercentType(50));
115 assertPublished("zigbee2mqtt/light/set/brightness", "128");
117 // OnOff commands should route to the correct topic
118 sendCommand(component, Light.COLOR_CHANNEL_ID, OnOffType.OFF);
119 assertPublished("zigbee2mqtt/light/set/state", "OFF_");
123 public void testRgbWithoutBrightness() throws InterruptedException {
125 var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
129 "state_topic": "zigbee2mqtt/light/state", \
130 "command_topic": "zigbee2mqtt/light/set/state", \
131 "state_value_template": "{{ value_json.power }}", \
132 "payload_on": "ON_", \
133 "payload_off": "OFF_", \
134 "rgb_state_topic": "zigbee2mqtt/light/rgb", \
135 "rgb_command_topic": "zigbee2mqtt/light/set/rgb", \
136 "rgb_value_template": "{{ value_json.rgb }}"\
141 publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"255,255,255\"}");
142 assertState(component, Light.COLOR_CHANNEL_ID, HSBType.WHITE);
144 // Brightness commands should route to the correct topic, converted to RGB
145 sendCommand(component, Light.COLOR_CHANNEL_ID, new PercentType(50));
146 assertPublished("zigbee2mqtt/light/set/rgb", "128,128,128");
148 // OnOff commands should route to the correct topic
149 sendCommand(component, Light.COLOR_CHANNEL_ID, OnOffType.OFF);
150 assertPublished("zigbee2mqtt/light/set/state", "OFF_");
154 public void testHsb() throws InterruptedException {
156 var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
160 "state_topic": "zigbee2mqtt/light/state", \
161 "command_topic": "zigbee2mqtt/light/set/state", \
162 "state_value_template": "{{ value_json.power }}", \
163 "payload_on": "ON_", \
164 "payload_off": "OFF_", \
165 "hs_state_topic": "zigbee2mqtt/light/hs", \
166 "hs_command_topic": "zigbee2mqtt/light/set/hs", \
167 "hs_value_template": "{{ value_json.hs }}", \
168 "brightness_state_topic": "zigbee2mqtt/light/brightness", \
169 "brightness_command_topic": "zigbee2mqtt/light/set/brightness", \
170 "brightness_value_template": "{{ value_json.br }}" \
175 assertThat(component.channels.size(), is(1));
176 assertThat(component.getName(), is("light"));
178 assertChannel(component, Light.COLOR_CHANNEL_ID, "", "dummy", "Color", ColorValue.class);
181 ComponentChannel onOffChannel = component.onOffChannel;
182 assertThat(onOffChannel, is(notNullValue()));
183 if (onOffChannel != null) {
184 assertChannel(onOffChannel, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state", "On/Off State",
188 ComponentChannel brightnessChannel = component.brightnessChannel;
189 assertThat(brightnessChannel, is(notNullValue()));
190 if (brightnessChannel != null) {
191 assertChannel(brightnessChannel, "zigbee2mqtt/light/brightness", "zigbee2mqtt/light/set/brightness",
192 "Brightness", PercentageValue.class);
195 publishMessage("zigbee2mqtt/light/hs", "{\"hs\": \"180,50\"}");
196 publishMessage("zigbee2mqtt/light/brightness", "{\"br\": \"128\"}");
197 assertState(component, Light.COLOR_CHANNEL_ID, new HSBType(new DecimalType(180), new PercentType(50),
198 new PercentType(new BigDecimal(128 * 100).divide(new BigDecimal(255), MathContext.DECIMAL128))));
200 sendCommand(component, Light.COLOR_CHANNEL_ID, HSBType.BLUE);
201 assertPublished("zigbee2mqtt/light/set/brightness", "255");
202 assertPublished("zigbee2mqtt/light/set/hs", "240,100");
204 // Brightness commands should route to the correct topic
205 sendCommand(component, Light.COLOR_CHANNEL_ID, new PercentType(50));
206 assertPublished("zigbee2mqtt/light/set/brightness", "128");
208 // OnOff commands should route to the correct topic
209 sendCommand(component, Light.COLOR_CHANNEL_ID, OnOffType.OFF);
210 assertPublished("zigbee2mqtt/light/set/state", "OFF_");
214 public void testBrightnessAndOnOff() throws InterruptedException {
216 var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
220 "state_topic": "zigbee2mqtt/light/state", \
221 "command_topic": "zigbee2mqtt/light/set/state", \
222 "state_value_template": "{{ value_json.power }}", \
223 "brightness_state_topic": "zigbee2mqtt/light/brightness", \
224 "brightness_command_topic": "zigbee2mqtt/light/set/brightness", \
225 "payload_on": "ON_", \
226 "payload_off": "OFF_" \
231 assertThat(component.channels.size(), is(1));
232 assertThat(component.getName(), is("light"));
234 assertChannel(component, Light.BRIGHTNESS_CHANNEL_ID, "zigbee2mqtt/light/brightness",
235 "zigbee2mqtt/light/set/brightness", "Brightness", PercentageValue.class);
237 ComponentChannel onOffChannel = component.onOffChannel;
238 assertThat(onOffChannel, is(notNullValue()));
239 if (onOffChannel != null) {
240 assertChannel(onOffChannel, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state", "On/Off State",
244 publishMessage("zigbee2mqtt/light/brightness", "128");
245 assertState(component, Light.BRIGHTNESS_CHANNEL_ID,
246 new PercentType(new BigDecimal(128 * 100).divide(new BigDecimal(255), MathContext.DECIMAL128)));
247 publishMessage("zigbee2mqtt/light/brightness", "64");
248 assertState(component, Light.BRIGHTNESS_CHANNEL_ID,
249 new PercentType(new BigDecimal(64 * 100).divide(new BigDecimal(255), MathContext.DECIMAL128)));
251 sendCommand(component, Light.BRIGHTNESS_CHANNEL_ID, OnOffType.OFF);
252 assertPublished("zigbee2mqtt/light/set/state", "OFF_");
254 sendCommand(component, Light.BRIGHTNESS_CHANNEL_ID, OnOffType.ON);
255 assertPublished("zigbee2mqtt/light/set/state", "ON_");
259 public void testOnOffOnly() throws InterruptedException {
261 var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
265 "state_topic": "zigbee2mqtt/light/state", \
266 "command_topic": "zigbee2mqtt/light/set/state", \
267 "state_value_template": "{{ value_json.power }}", \
268 "payload_on": "ON_", \
269 "payload_off": "OFF_" \
274 assertThat(component.channels.size(), is(1));
275 assertThat(component.getName(), is("light"));
277 assertChannel(component, Light.ON_OFF_CHANNEL_ID, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state",
278 "On/Off State", OnOffValue.class);
279 assertThat(component.brightnessChannel, is(nullValue()));
281 publishMessage("zigbee2mqtt/light/state", "{\"power\": \"ON_\"}");
282 assertState(component, Light.ON_OFF_CHANNEL_ID, OnOffType.ON);
283 publishMessage("zigbee2mqtt/light/state", "{\"power\": \"OFF_\"}");
284 assertState(component, Light.ON_OFF_CHANNEL_ID, OnOffType.OFF);
286 sendCommand(component, Light.ON_OFF_CHANNEL_ID, OnOffType.OFF);
287 assertPublished("zigbee2mqtt/light/set/state", "OFF_");
291 public void testOnOffWithEffect() throws InterruptedException {
293 var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
295 " \"name\": \"light\", " +
296 " \"state_topic\": \"zigbee2mqtt/light/state\", " +
297 " \"command_topic\": \"zigbee2mqtt/light/set/state\", " +
298 " \"effect_command_topic\": \"zigbee2mqtt/light/set/effect\", " +
299 " \"state_value_template\": \"{{ value_json.power }}\", " +
300 " \"effect_list\": [\"party\", \"rainbow\"]," +
301 " \"effect_state_topic\": \"zigbee2mqtt/light/effect\"" +
305 assertThat(component.channels.size(), is(2));
306 assertThat(component.getName(), is("light"));
308 assertChannel(component, Light.ON_OFF_CHANNEL_ID, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state",
309 "On/Off State", OnOffValue.class);
310 assertChannel(component, Light.EFFECT_CHANNEL_ID, "zigbee2mqtt/light/effect", "zigbee2mqtt/light/set/effect",
311 "Lighting Effect", TextValue.class);
313 publishMessage("zigbee2mqtt/light/state", "{\"power\": \"ON\"}");
314 assertState(component, Light.ON_OFF_CHANNEL_ID, OnOffType.ON);
315 publishMessage("zigbee2mqtt/light/effect", "party");
316 assertState(component, Light.EFFECT_CHANNEL_ID, new StringType("party"));
317 publishMessage("zigbee2mqtt/light/state", "{\"power\": \"OFF\"}");
318 assertState(component, Light.ON_OFF_CHANNEL_ID, OnOffType.OFF);
320 sendCommand(component, Light.EFFECT_CHANNEL_ID, new StringType("rainbow"));
321 assertPublished("zigbee2mqtt/light/set/effect", "rainbow");
325 protected Set<String> getConfigTopics() {
326 return Set.of(CONFIG_TOPIC);