]> git.basschouten.com Git - openhab-addons.git/blob
dd07a6962e8ecbbdcfcd0ddebd3f5e5f1b9e1b51
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mqtt;
14
15 import static org.hamcrest.CoreMatchers.is;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.junit.jupiter.api.Assertions.*;
18 import static org.mockito.ArgumentMatchers.any;
19 import static org.mockito.Mockito.*;
20 import static org.mockito.MockitoAnnotations.openMocks;
21
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.concurrent.CompletableFuture;
28 import java.util.concurrent.CountDownLatch;
29 import java.util.concurrent.ExecutionException;
30 import java.util.concurrent.ScheduledExecutorService;
31 import java.util.concurrent.ScheduledThreadPoolExecutor;
32 import java.util.concurrent.TimeUnit;
33 import java.util.concurrent.TimeoutException;
34
35 import org.junit.jupiter.api.AfterEach;
36 import org.junit.jupiter.api.BeforeEach;
37 import org.junit.jupiter.api.Test;
38 import org.mockito.Mock;
39 import org.openhab.binding.mqtt.generic.AvailabilityTracker;
40 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
41 import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
42 import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
43 import org.openhab.binding.mqtt.homeassistant.internal.AbstractComponent;
44 import org.openhab.binding.mqtt.homeassistant.internal.ChannelConfigurationTypeAdapterFactory;
45 import org.openhab.binding.mqtt.homeassistant.internal.ComponentSwitch;
46 import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents;
47 import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.ComponentDiscovered;
48 import org.openhab.binding.mqtt.homeassistant.internal.HaID;
49 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
50 import org.openhab.core.io.transport.mqtt.MqttConnectionObserver;
51 import org.openhab.core.io.transport.mqtt.MqttConnectionState;
52 import org.openhab.core.io.transport.mqtt.MqttService;
53 import org.openhab.core.library.types.OnOffType;
54 import org.openhab.core.test.java.JavaOSGiTest;
55 import org.openhab.core.types.State;
56 import org.openhab.core.types.UnDefType;
57 import org.openhab.core.util.UIDUtils;
58
59 import com.google.gson.Gson;
60 import com.google.gson.GsonBuilder;
61
62 /**
63  * A full implementation test, that starts the embedded MQTT broker and publishes a homeassistant MQTT discovery device
64  * tree.
65  *
66  * @author David Graeff - Initial contribution
67  */
68 public class HomeAssistantMQTTImplementationTest extends JavaOSGiTest {
69     private MqttService mqttService;
70     private MqttBrokerConnection embeddedConnection;
71     private MqttBrokerConnection connection;
72     private int registeredTopics = 100;
73     private Throwable failure = null;
74
75     private AutoCloseable mocksCloseable;
76
77     private @Mock ChannelStateUpdateListener channelStateUpdateListener;
78     private @Mock AvailabilityTracker availabilityTracker;
79     private @Mock TransformationServiceProvider transformationServiceProvider;
80
81     /**
82      * Create an observer that fails the test as soon as the broker client connection changes its connection state
83      * to something else then CONNECTED.
84      */
85     private MqttConnectionObserver failIfChange = (state, error) -> assertThat(state,
86             is(MqttConnectionState.CONNECTED));
87     private String testObjectTopic;
88
89     @BeforeEach
90     public void beforeEach() throws Exception {
91         registerVolatileStorageService();
92         mocksCloseable = openMocks(this);
93         mqttService = getService(MqttService.class);
94
95         // Wait for the EmbeddedBrokerService internal connection to be connected
96         embeddedConnection = new EmbeddedBrokerTools().waitForConnection(mqttService);
97
98         connection = new MqttBrokerConnection(embeddedConnection.getHost(), embeddedConnection.getPort(),
99                 embeddedConnection.isSecure(), "ha_mqtt");
100         connection.start().get(1000, TimeUnit.MILLISECONDS);
101         assertThat(connection.connectionState(), is(MqttConnectionState.CONNECTED));
102
103         // If the connection state changes in between -> fail
104         connection.addConnectionObserver(failIfChange);
105
106         // Create topic string and config for one example HA component (a Switch)
107         testObjectTopic = "homeassistant/switch/node/" + ThingChannelConstants.testHomeAssistantThing.getId();
108         final String config = "{'name':'testname','state_topic':'" + testObjectTopic + "/state','command_topic':'"
109                 + testObjectTopic + "/set'}";
110
111         // Publish component configurations and component states to MQTT
112         List<CompletableFuture<Boolean>> futures = new ArrayList<>();
113         futures.add(embeddedConnection.publish(testObjectTopic + "/config", config.getBytes()));
114         futures.add(embeddedConnection.publish(testObjectTopic + "/state", "true".getBytes()));
115
116         registeredTopics = futures.size();
117         CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(1000, TimeUnit.MILLISECONDS);
118
119         failure = null;
120
121         doReturn(null).when(transformationServiceProvider).getTransformationService(any());
122     }
123
124     @AfterEach
125     public void afterEach() throws Exception {
126         if (connection != null) {
127             connection.removeConnectionObserver(failIfChange);
128             connection.stop().get(1000, TimeUnit.MILLISECONDS);
129         }
130
131         mocksCloseable.close();
132     }
133
134     @Test
135     public void reconnectTest() throws InterruptedException, ExecutionException, TimeoutException {
136         connection.removeConnectionObserver(failIfChange);
137         connection.stop().get(2000, TimeUnit.MILLISECONDS);
138         connection = new MqttBrokerConnection(embeddedConnection.getHost(), embeddedConnection.getPort(),
139                 embeddedConnection.isSecure(), "ha_mqtt");
140         connection.start().get(2000, TimeUnit.MILLISECONDS);
141     }
142
143     @Test
144     public void retrieveAllTopics() throws InterruptedException, ExecutionException, TimeoutException {
145         CountDownLatch c = new CountDownLatch(registeredTopics);
146         connection.subscribe("homeassistant/+/+/" + ThingChannelConstants.testHomeAssistantThing.getId() + "/#",
147                 (topic, payload) -> c.countDown()).get(1000, TimeUnit.MILLISECONDS);
148         assertTrue(c.await(1000, TimeUnit.MILLISECONDS),
149                 "Connection " + connection.getClientId() + " not retrieving all topics");
150     }
151
152     @Test
153     public void parseHATree() throws InterruptedException, ExecutionException, TimeoutException {
154         MqttChannelTypeProvider channelTypeProvider = mock(MqttChannelTypeProvider.class);
155
156         final Map<String, AbstractComponent<?>> haComponents = new HashMap<>();
157         Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
158
159         ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4);
160         DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.testHomeAssistantThing,
161                 scheduler, channelStateUpdateListener, availabilityTracker, gson, transformationServiceProvider));
162
163         // The DiscoverComponents object calls ComponentDiscovered callbacks.
164         // In the following implementation we add the found component to the `haComponents` map
165         // and add the types to the channelTypeProvider, like in the real Thing handler.
166         final CountDownLatch latch = new CountDownLatch(1);
167         ComponentDiscovered cd = (haID, c) -> {
168             haComponents.put(c.uid().getId(), c);
169             c.addChannelTypes(channelTypeProvider);
170             channelTypeProvider.setChannelGroupType(c.groupTypeUID(), c.type());
171             latch.countDown();
172         };
173
174         // Start the discovery for 500ms. Forced timeout after 1500ms.
175         HaID haID = new HaID(testObjectTopic + "/config");
176         CompletableFuture<Void> future = discover.startDiscovery(connection, 1000, Collections.singleton(haID), cd)
177                 .thenRun(() -> {
178                 }).exceptionally(e -> {
179                     failure = e;
180                     return null;
181                 });
182
183         assertTrue(latch.await(1500, TimeUnit.MILLISECONDS));
184         future.get(800, TimeUnit.MILLISECONDS);
185
186         // No failure expected and one discovered result
187         assertNull(failure);
188         assertThat(haComponents.size(), is(1));
189
190         // For the switch component we should have one channel group type and one channel type
191         // setChannelGroupType is called once above
192         verify(channelTypeProvider, times(2)).setChannelGroupType(any(), any());
193         verify(channelTypeProvider, times(1)).setChannelType(any(), any());
194
195         String channelGroupId = UIDUtils
196                 .encode("node_" + ThingChannelConstants.testHomeAssistantThing.getId() + "_switch");
197
198         State value = haComponents.get(channelGroupId).channelTypes().get(ComponentSwitch.switchChannelID).getState()
199                 .getCache().getChannelState();
200         assertThat(value, is(UnDefType.UNDEF));
201
202         haComponents.values().stream().map(e -> e.start(connection, scheduler, 100))
203                 .reduce(CompletableFuture.completedFuture(null), (a, v) -> a.thenCompose(b -> v)).exceptionally(e -> {
204                     failure = e;
205                     return null;
206                 }).get();
207
208         // We should have received the retained value, while subscribing to the channels MQTT state topic.
209         verify(channelStateUpdateListener, timeout(1000).times(1)).updateChannelState(any(), any());
210
211         // Value should be ON now.
212         value = haComponents.get(channelGroupId).channelTypes().get(ComponentSwitch.switchChannelID).getState()
213                 .getCache().getChannelState();
214         assertThat(value, is(OnOffType.ON));
215     }
216 }