2 * Copyright (c) 2010-2021 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.handler;
15 import static org.hamcrest.MatcherAssert.assertThat;
16 import static org.mockito.Mockito.any;
17 import static org.mockito.Mockito.eq;
18 import static org.mockito.Mockito.never;
19 import static org.mockito.Mockito.spy;
20 import static org.mockito.Mockito.timeout;
21 import static org.mockito.Mockito.times;
22 import static org.mockito.Mockito.verify;
23 import static org.mockito.Mockito.when;
25 import java.nio.charset.StandardCharsets;
26 import java.util.Arrays;
27 import java.util.List;
28 import java.util.stream.Collectors;
30 import org.hamcrest.CoreMatchers;
31 import org.junit.jupiter.api.BeforeEach;
32 import org.junit.jupiter.api.Test;
33 import org.junit.jupiter.api.extension.ExtendWith;
34 import org.mockito.Mock;
35 import org.mockito.junit.jupiter.MockitoExtension;
36 import org.openhab.binding.mqtt.homeassistant.internal.AbstractHomeAssistantTests;
37 import org.openhab.binding.mqtt.homeassistant.internal.HaID;
38 import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
39 import org.openhab.binding.mqtt.homeassistant.internal.component.Climate;
40 import org.openhab.binding.mqtt.homeassistant.internal.component.Switch;
41 import org.openhab.core.thing.binding.ThingHandlerCallback;
44 * Tests for {@link HomeAssistantThingHandler}
46 * @author Anton Kharuzhy - Initial contribution
48 @SuppressWarnings({ "ConstantConditions" })
49 @ExtendWith(MockitoExtension.class)
50 public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
51 private static final int SUBSCRIBE_TIMEOUT = 10000;
52 private static final int ATTRIBUTE_RECEIVE_TIMEOUT = 2000;
54 private static final List<String> CONFIG_TOPICS = Arrays.asList("climate/0x847127fffe11dd6a_climate_zigbee2mqtt",
55 "switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt",
57 "sensor/0x1111111111111111_test_sensor_zigbee2mqtt", "camera/0x1111111111111111_test_camera_zigbee2mqtt",
59 "cover/0x2222222222222222_test_cover_zigbee2mqtt", "fan/0x2222222222222222_test_fan_zigbee2mqtt",
60 "light/0x2222222222222222_test_light_zigbee2mqtt", "lock/0x2222222222222222_test_lock_zigbee2mqtt");
62 private static final List<String> MQTT_TOPICS = CONFIG_TOPICS.stream()
63 .map(AbstractHomeAssistantTests::configTopicToMqtt).collect(Collectors.toList());
65 private @Mock ThingHandlerCallback callback;
66 private HomeAssistantThingHandler thingHandler;
70 final var config = haThing.getConfiguration();
72 config.put(HandlerConfiguration.PROPERTY_BASETOPIC, HandlerConfiguration.DEFAULT_BASETOPIC);
73 config.put(HandlerConfiguration.PROPERTY_TOPICS, CONFIG_TOPICS);
75 when(callback.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
77 thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, transformationServiceProvider,
78 SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
79 thingHandler.setConnection(bridgeConnection);
80 thingHandler.setCallback(callback);
81 thingHandler = spy(thingHandler);
85 public void testInitialize() {
87 thingHandler.initialize();
89 verify(callback).statusUpdated(eq(haThing), any());
90 // Expect a call to the bridge status changed, the start, the propertiesChanged method
91 verify(thingHandler).bridgeStatusChanged(any());
92 verify(thingHandler, timeout(SUBSCRIBE_TIMEOUT)).start(any());
94 // Expect subscription on each topic from config
95 MQTT_TOPICS.forEach(t -> {
96 verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).subscribe(eq(t), any());
99 verify(thingHandler, never()).componentDiscovered(any(), any());
100 assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
101 // Components discovered after messages in corresponding topics
102 var configTopic = "homeassistant/climate/0x847127fffe11dd6a_climate_zigbee2mqtt/config";
103 thingHandler.discoverComponents.processMessage(configTopic,
104 getResourceAsByteArray("component/configTS0601ClimateThermostat.json"));
105 verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopic)), any(Climate.class));
107 thingHandler.delayedProcessing.forceProcessNow();
108 assertThat(haThing.getChannels().size(), CoreMatchers.is(6));
109 verify(channelTypeProvider, times(6)).setChannelType(any(), any());
110 verify(channelTypeProvider, times(1)).setChannelGroupType(any(), any());
112 configTopic = "homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config";
113 thingHandler.discoverComponents.processMessage(configTopic,
114 getResourceAsByteArray("component/configTS0601AutoLock.json"));
115 verify(thingHandler, times(2)).componentDiscovered(any(), any());
116 verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopic)), any(Switch.class));
118 thingHandler.delayedProcessing.forceProcessNow();
119 assertThat(haThing.getChannels().size(), CoreMatchers.is(7));
120 verify(channelTypeProvider, times(7)).setChannelType(any(), any());
121 verify(channelTypeProvider, times(2)).setChannelGroupType(any(), any());
125 public void testDispose() {
126 thingHandler.initialize();
128 // Expect subscription on each topic from config
129 CONFIG_TOPICS.forEach(t -> {
130 var fullTopic = HandlerConfiguration.DEFAULT_BASETOPIC + "/" + t + "/config";
131 verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).subscribe(eq(fullTopic), any());
133 thingHandler.discoverComponents.processMessage(
134 "homeassistant/climate/0x847127fffe11dd6a_climate_zigbee2mqtt/config",
135 getResourceAsByteArray("component/configTS0601ClimateThermostat.json"));
136 thingHandler.discoverComponents.processMessage(
137 "homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config",
138 getResourceAsByteArray("component/configTS0601AutoLock.json"));
139 thingHandler.delayedProcessing.forceProcessNow();
140 assertThat(haThing.getChannels().size(), CoreMatchers.is(7));
141 verify(channelTypeProvider, times(7)).setChannelType(any(), any());
144 thingHandler.dispose();
146 // Expect unsubscription on each topic from config
147 MQTT_TOPICS.forEach(t -> {
148 verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).unsubscribe(eq(t), any());
151 // Expect channel types removed, 6 for climate and 1 for switch
152 verify(channelTypeProvider, times(7)).removeChannelType(any());
153 // Expect channel group types removed, 1 for each component
154 verify(channelTypeProvider, times(2)).removeChannelGroupType(any());
158 public void testProcessMessageFromUnsupportedComponent() {
159 thingHandler.initialize();
160 thingHandler.discoverComponents.processMessage("homeassistant/unsupportedType/id_zigbee2mqtt/config",
161 "{}".getBytes(StandardCharsets.UTF_8));
162 // Ignore unsupported component
163 thingHandler.delayedProcessing.forceProcessNow();
164 assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
168 public void testProcessMessageWithEmptyConfig() {
169 thingHandler.initialize();
170 thingHandler.discoverComponents.processMessage("homeassistant/sensor/id_zigbee2mqtt/config",
171 "".getBytes(StandardCharsets.UTF_8));
172 // Ignore component with empty config
173 thingHandler.delayedProcessing.forceProcessNow();
174 assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
178 public void testProcessMessageWithBadFormatConfig() {
179 thingHandler.initialize();
180 thingHandler.discoverComponents.processMessage("homeassistant/sensor/id_zigbee2mqtt/config",
181 "{bad format}}".getBytes(StandardCharsets.UTF_8));
182 // Ignore component with bad format config
183 thingHandler.delayedProcessing.forceProcessNow();
184 assertThat(haThing.getChannels().size(), CoreMatchers.is(0));