2 * Copyright (c) 2010-2022 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.ArgumentMatchers.*;
17 import static org.mockito.Mockito.*;
19 import java.nio.charset.StandardCharsets;
20 import java.util.Arrays;
21 import java.util.List;
22 import java.util.stream.Collectors;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.hamcrest.CoreMatchers;
26 import org.junit.jupiter.api.BeforeEach;
27 import org.junit.jupiter.api.Test;
28 import org.junit.jupiter.api.extension.ExtendWith;
29 import org.mockito.Mock;
30 import org.mockito.junit.jupiter.MockitoExtension;
31 import org.openhab.binding.mqtt.homeassistant.internal.AbstractHomeAssistantTests;
32 import org.openhab.binding.mqtt.homeassistant.internal.HaID;
33 import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
34 import org.openhab.binding.mqtt.homeassistant.internal.component.Climate;
35 import org.openhab.binding.mqtt.homeassistant.internal.component.Switch;
36 import org.openhab.core.thing.binding.ThingHandlerCallback;
39 * Tests for {@link HomeAssistantThingHandler}
41 * @author Anton Kharuzhy - Initial contribution
43 @SuppressWarnings({ "ConstantConditions" })
44 @ExtendWith(MockitoExtension.class)
46 public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
47 private static final int SUBSCRIBE_TIMEOUT = 10000;
48 private static final int ATTRIBUTE_RECEIVE_TIMEOUT = 2000;
50 private static final List<String> CONFIG_TOPICS = Arrays.asList("climate/0x847127fffe11dd6a_climate_zigbee2mqtt",
51 "switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt",
53 "sensor/0x1111111111111111_test_sensor_zigbee2mqtt", "camera/0x1111111111111111_test_camera_zigbee2mqtt",
55 "cover/0x2222222222222222_test_cover_zigbee2mqtt", "fan/0x2222222222222222_test_fan_zigbee2mqtt",
56 "light/0x2222222222222222_test_light_zigbee2mqtt", "lock/0x2222222222222222_test_lock_zigbee2mqtt");
58 private static final List<String> MQTT_TOPICS = CONFIG_TOPICS.stream()
59 .map(AbstractHomeAssistantTests::configTopicToMqtt).collect(Collectors.toList());
61 private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
62 private @NonNullByDefault({}) HomeAssistantThingHandler thingHandler;
66 final var config = haThing.getConfiguration();
68 config.put(HandlerConfiguration.PROPERTY_BASETOPIC, HandlerConfiguration.DEFAULT_BASETOPIC);
69 config.put(HandlerConfiguration.PROPERTY_TOPICS, CONFIG_TOPICS);
71 when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
73 thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, transformationServiceProvider,
74 SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
75 thingHandler.setConnection(bridgeConnection);
76 thingHandler.setCallback(callbackMock);
77 thingHandler = spy(thingHandler);
81 public void testInitialize() {
83 thingHandler.initialize();
85 verify(callbackMock).statusUpdated(eq(haThing), any());
86 // Expect a call to the bridge status changed, the start, the propertiesChanged method
87 verify(thingHandler).bridgeStatusChanged(any());
88 verify(thingHandler, timeout(SUBSCRIBE_TIMEOUT)).start(any());
90 // Expect subscription on each topic from config
91 MQTT_TOPICS.forEach(t -> {
92 verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).subscribe(eq(t), any());
95 verify(thingHandler, never()).componentDiscovered(any(), any());
96 assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
97 // Components discovered after messages in corresponding topics
98 var configTopic = "homeassistant/climate/0x847127fffe11dd6a_climate_zigbee2mqtt/config";
99 thingHandler.discoverComponents.processMessage(configTopic,
100 getResourceAsByteArray("component/configTS0601ClimateThermostat.json"));
101 verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopic)), any(Climate.class));
103 thingHandler.delayedProcessing.forceProcessNow();
104 assertThat(haThing.getChannels().size(), CoreMatchers.is(6));
105 verify(channelTypeProvider, times(6)).setChannelType(any(), any());
106 verify(channelTypeProvider, times(1)).setChannelGroupType(any(), any());
108 configTopic = "homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config";
109 thingHandler.discoverComponents.processMessage(configTopic,
110 getResourceAsByteArray("component/configTS0601AutoLock.json"));
111 verify(thingHandler, times(2)).componentDiscovered(any(), any());
112 verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopic)), any(Switch.class));
114 thingHandler.delayedProcessing.forceProcessNow();
115 assertThat(haThing.getChannels().size(), CoreMatchers.is(7));
116 verify(channelTypeProvider, times(7)).setChannelType(any(), any());
117 verify(channelTypeProvider, times(2)).setChannelGroupType(any(), any());
121 public void testDispose() {
122 thingHandler.initialize();
124 // Expect subscription on each topic from config
125 CONFIG_TOPICS.forEach(t -> {
126 var fullTopic = HandlerConfiguration.DEFAULT_BASETOPIC + "/" + t + "/config";
127 verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).subscribe(eq(fullTopic), any());
129 thingHandler.discoverComponents.processMessage(
130 "homeassistant/climate/0x847127fffe11dd6a_climate_zigbee2mqtt/config",
131 getResourceAsByteArray("component/configTS0601ClimateThermostat.json"));
132 thingHandler.discoverComponents.processMessage(
133 "homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config",
134 getResourceAsByteArray("component/configTS0601AutoLock.json"));
135 thingHandler.delayedProcessing.forceProcessNow();
136 assertThat(haThing.getChannels().size(), CoreMatchers.is(7));
137 verify(channelTypeProvider, times(7)).setChannelType(any(), any());
140 thingHandler.dispose();
142 // Expect unsubscription on each topic from config
143 MQTT_TOPICS.forEach(t -> {
144 verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).unsubscribe(eq(t), any());
147 // Expect channel types removed, 6 for climate and 1 for switch
148 verify(channelTypeProvider, times(7)).removeChannelType(any());
149 // Expect channel group types removed, 1 for each component
150 verify(channelTypeProvider, times(2)).removeChannelGroupType(any());
154 public void testProcessMessageFromUnsupportedComponent() {
155 thingHandler.initialize();
156 thingHandler.discoverComponents.processMessage("homeassistant/unsupportedType/id_zigbee2mqtt/config",
157 "{}".getBytes(StandardCharsets.UTF_8));
158 // Ignore unsupported component
159 thingHandler.delayedProcessing.forceProcessNow();
160 assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
164 public void testProcessMessageWithEmptyConfig() {
165 thingHandler.initialize();
166 thingHandler.discoverComponents.processMessage("homeassistant/sensor/id_zigbee2mqtt/config",
167 "".getBytes(StandardCharsets.UTF_8));
168 // Ignore component with empty config
169 thingHandler.delayedProcessing.forceProcessNow();
170 assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
174 public void testProcessMessageWithBadFormatConfig() {
175 thingHandler.initialize();
176 thingHandler.discoverComponents.processMessage("homeassistant/sensor/id_zigbee2mqtt/config",
177 "{bad format}}".getBytes(StandardCharsets.UTF_8));
178 // Ignore component with bad format config
179 thingHandler.delayedProcessing.forceProcessNow();
180 assertThat(haThing.getChannels().size(), CoreMatchers.is(0));