]> git.basschouten.com Git - openhab-addons.git/blob
042c14a51f2c42bd1aa036d41d12c2dd7d5b8ebb
[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.homie.internal.handler;
14
15 import static org.hamcrest.CoreMatchers.is;
16 import static org.junit.Assert.*;
17 import static org.mockito.ArgumentMatchers.*;
18 import static org.mockito.Mockito.*;
19 import static org.openhab.binding.mqtt.homie.internal.handler.ThingChannelConstants.TEST_HOMIE_THING;
20
21 import java.lang.reflect.Field;
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.concurrent.CompletableFuture;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.ScheduledExecutorService;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30
31 import org.eclipse.jdt.annotation.NonNull;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.junit.Before;
34 import org.junit.Test;
35 import org.mockito.Mock;
36 import org.mockito.MockitoAnnotations;
37 import org.mockito.invocation.InvocationOnMock;
38 import org.openhab.binding.mqtt.generic.ChannelState;
39 import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
40 import org.openhab.binding.mqtt.generic.mapping.AbstractMqttAttributeClass;
41 import org.openhab.binding.mqtt.generic.mapping.SubscribeFieldToMQTTtopic;
42 import org.openhab.binding.mqtt.generic.tools.ChildMap;
43 import org.openhab.binding.mqtt.generic.tools.DelayedBatchProcessing;
44 import org.openhab.binding.mqtt.generic.values.Value;
45 import org.openhab.binding.mqtt.handler.AbstractBrokerHandler;
46 import org.openhab.binding.mqtt.homie.ChannelStateHelper;
47 import org.openhab.binding.mqtt.homie.ThingHandlerHelper;
48 import org.openhab.binding.mqtt.homie.generic.internal.MqttBindingConstants;
49 import org.openhab.binding.mqtt.homie.internal.homie300.Device;
50 import org.openhab.binding.mqtt.homie.internal.homie300.DeviceAttributes;
51 import org.openhab.binding.mqtt.homie.internal.homie300.DeviceAttributes.ReadyState;
52 import org.openhab.binding.mqtt.homie.internal.homie300.Node;
53 import org.openhab.binding.mqtt.homie.internal.homie300.NodeAttributes;
54 import org.openhab.binding.mqtt.homie.internal.homie300.Property;
55 import org.openhab.binding.mqtt.homie.internal.homie300.PropertyAttributes;
56 import org.openhab.binding.mqtt.homie.internal.homie300.PropertyAttributes.DataTypeEnum;
57 import org.openhab.core.config.core.Configuration;
58 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
59 import org.openhab.core.library.types.StringType;
60 import org.openhab.core.thing.Channel;
61 import org.openhab.core.thing.Thing;
62 import org.openhab.core.thing.ThingStatus;
63 import org.openhab.core.thing.ThingStatusDetail;
64 import org.openhab.core.thing.ThingStatusInfo;
65 import org.openhab.core.thing.binding.ThingHandlerCallback;
66 import org.openhab.core.thing.binding.builder.ThingBuilder;
67 import org.openhab.core.thing.type.ChannelKind;
68 import org.openhab.core.thing.type.ThingTypeRegistry;
69 import org.openhab.core.types.Command;
70 import org.openhab.core.types.RefreshType;
71 import org.openhab.core.types.TypeParser;
72
73 /**
74  * Tests cases for {@link HomieThingHandler}.
75  *
76  * @author David Graeff - Initial contribution
77  */
78 public class HomieThingHandlerTests {
79     @Mock
80     private ThingHandlerCallback callback;
81
82     private Thing thing;
83
84     @Mock
85     private AbstractBrokerHandler bridgeHandler;
86
87     @Mock
88     private MqttBrokerConnection connection;
89
90     @Mock
91     private ScheduledExecutorService scheduler;
92
93     @Mock
94     private ScheduledFuture<?> scheduledFuture;
95
96     @Mock
97     private ThingTypeRegistry thingTypeRegistry;
98
99     private HomieThingHandler thingHandler;
100
101     private final MqttChannelTypeProvider channelTypeProvider = new MqttChannelTypeProvider(thingTypeRegistry);
102
103     private final String deviceID = ThingChannelConstants.TEST_HOMIE_THING.getId();
104     private final String deviceTopic = "homie/" + deviceID;
105
106     // A completed future is returned for a subscribe call to the attributes
107     CompletableFuture<@Nullable Void> future = CompletableFuture.completedFuture(null);
108
109     @Before
110     public void setUp() {
111         final ThingStatusInfo thingStatus = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
112
113         MockitoAnnotations.initMocks(this);
114
115         final Configuration config = new Configuration();
116         config.put("basetopic", "homie");
117         config.put("deviceid", deviceID);
118
119         thing = ThingBuilder.create(MqttBindingConstants.HOMIE300_MQTT_THING, TEST_HOMIE_THING.getId())
120                 .withConfiguration(config).build();
121         thing.setStatusInfo(thingStatus);
122
123         // Return the mocked connection object if the bridge handler is asked for it
124         when(bridgeHandler.getConnectionAsync()).thenReturn(CompletableFuture.completedFuture(connection));
125
126         doReturn(CompletableFuture.completedFuture(true)).when(connection).subscribe(any(), any());
127         doReturn(CompletableFuture.completedFuture(true)).when(connection).unsubscribe(any(), any());
128         doReturn(CompletableFuture.completedFuture(true)).when(connection).unsubscribeAll();
129         doReturn(CompletableFuture.completedFuture(true)).when(connection).publish(any(), any(), anyInt(),
130                 anyBoolean());
131
132         doReturn(false).when(scheduledFuture).isDone();
133         doReturn(scheduledFuture).when(scheduler).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class));
134
135         final HomieThingHandler handler = new HomieThingHandler(thing, channelTypeProvider, 1000, 30, 5);
136         thingHandler = spy(handler);
137         thingHandler.setCallback(callback);
138         final Device device = new Device(thing.getUID(), thingHandler, spy(new DeviceAttributes()),
139                 spy(new ChildMap<>()));
140         thingHandler.setInternalObjects(spy(device), spy(new DelayedBatchProcessing<>(500, thingHandler, scheduler)));
141
142         // Return the bridge handler if the thing handler asks for it
143         doReturn(bridgeHandler).when(thingHandler).getBridgeHandler();
144
145         // We are by default online
146         doReturn(thingStatus).when(thingHandler).getBridgeStatus();
147     }
148
149     @Test
150     public void initialize() {
151         assertThat(thingHandler.device.isInitialized(), is(false));
152         // // A completed future is returned for a subscribe call to the attributes
153         doReturn(future).when(thingHandler.device.attributes).subscribeAndReceive(any(), any(), anyString(), any(),
154                 anyInt());
155         doReturn(future).when(thingHandler.device.attributes).unsubscribe();
156         // Prevent a call to accept, that would update our thing.
157         doNothing().when(thingHandler).accept(any());
158         // Pretend that a device state change arrived.
159         thingHandler.device.attributes.state = ReadyState.ready;
160
161         verify(callback, times(0)).statusUpdated(eq(thing), any());
162
163         thingHandler.initialize();
164
165         // Expect a call to the bridge status changed, the start, the propertiesChanged method
166         verify(thingHandler).bridgeStatusChanged(any());
167         verify(thingHandler).start(any());
168         verify(thingHandler).readyStateChanged(any());
169         verify(thingHandler.device.attributes).subscribeAndReceive(any(), any(),
170                 argThat(arg -> deviceTopic.equals(arg)), any(), anyInt());
171
172         assertThat(thingHandler.device.isInitialized(), is(true));
173
174         verify(callback).statusUpdated(eq(thing), argThat((arg) -> arg.getStatus().equals(ThingStatus.ONLINE)
175                 && arg.getStatusDetail().equals(ThingStatusDetail.NONE)));
176     }
177
178     @Test
179     public void initializeGeneralTimeout() throws InterruptedException {
180         // A non completed future is returned for a subscribe call to the attributes
181         doReturn(future).when(thingHandler.device.attributes).subscribeAndReceive(any(), any(), anyString(), any(),
182                 anyInt());
183         doReturn(future).when(thingHandler.device.attributes).unsubscribe();
184
185         // Prevent a call to accept, that would update our thing.
186         doNothing().when(thingHandler).accept(any());
187
188         thingHandler.initialize();
189
190         verify(callback).statusUpdated(eq(thing), argThat((arg) -> arg.getStatus().equals(ThingStatus.OFFLINE)
191                 && arg.getStatusDetail().equals(ThingStatusDetail.COMMUNICATION_ERROR)));
192     }
193
194     @Test
195     public void initializeNoStateReceived() throws InterruptedException {
196         // A completed future is returned for a subscribe call to the attributes
197         doReturn(future).when(thingHandler.device.attributes).subscribeAndReceive(any(), any(), anyString(), any(),
198                 anyInt());
199         doReturn(future).when(thingHandler.device.attributes).unsubscribe();
200
201         // Prevent a call to accept, that would update our thing.
202         doNothing().when(thingHandler).accept(any());
203
204         thingHandler.initialize();
205         assertThat(thingHandler.device.isInitialized(), is(true));
206
207         verify(callback).statusUpdated(eq(thing), argThat((arg) -> arg.getStatus().equals(ThingStatus.OFFLINE)
208                 && arg.getStatusDetail().equals(ThingStatusDetail.GONE)));
209     }
210
211     @SuppressWarnings("null")
212     @Test
213     public void handleCommandRefresh() {
214         // Create mocked homie device tree with one node and one read-only property
215         Node node = thingHandler.device.createNode("node", spy(new NodeAttributes()));
216         doReturn(future).when(node.attributes).subscribeAndReceive(any(), any(), anyString(), any(), anyInt());
217         doReturn(future).when(node.attributes).unsubscribe();
218         node.attributes.name = "testnode";
219
220         Property property = node.createProperty("property", spy(new PropertyAttributes()));
221         doReturn(future).when(property.attributes).subscribeAndReceive(any(), any(), anyString(), any(), anyInt());
222         doReturn(future).when(property.attributes).unsubscribe();
223         property.attributes.name = "testprop";
224         property.attributes.datatype = DataTypeEnum.string_;
225         property.attributes.settable = false;
226         property.attributesReceived();
227         node.properties.put(property.propertyID, property);
228         thingHandler.device.nodes.put(node.nodeID, node);
229
230         ThingHandlerHelper.setConnection(thingHandler, connection);
231         // we need to set a channel value first, undefined values ignored on REFRESH
232         property.getChannelState().getCache().update(new StringType("testString"));
233
234         thingHandler.handleCommand(property.channelUID, RefreshType.REFRESH);
235         verify(callback).stateUpdated(argThat(arg -> property.channelUID.equals(arg)),
236                 argThat(arg -> property.getChannelState().getCache().getChannelState().equals(arg)));
237     }
238
239     @SuppressWarnings("null")
240     @Test
241     public void handleCommandUpdate() {
242         // Create mocked homie device tree with one node and one writable property
243         Node node = thingHandler.device.createNode("node", spy(new NodeAttributes()));
244         doReturn(future).when(node.attributes).subscribeAndReceive(any(), any(), anyString(), any(), anyInt());
245         doReturn(future).when(node.attributes).unsubscribe();
246         node.attributes.name = "testnode";
247
248         Property property = node.createProperty("property", spy(new PropertyAttributes()));
249         doReturn(future).when(property.attributes).subscribeAndReceive(any(), any(), anyString(), any(), anyInt());
250         doReturn(future).when(property.attributes).unsubscribe();
251         property.attributes.name = "testprop";
252         property.attributes.datatype = DataTypeEnum.string_;
253         property.attributes.settable = true;
254         property.attributesReceived();
255         node.properties.put(property.propertyID, property);
256         thingHandler.device.nodes.put(node.nodeID, node);
257
258         ChannelState channelState = property.getChannelState();
259         assertNotNull(channelState);
260         ChannelStateHelper.setConnection(channelState, connection);// Pretend we called start()
261         ThingHandlerHelper.setConnection(thingHandler, connection);
262
263         StringType updateValue = new StringType("UPDATE");
264         thingHandler.handleCommand(property.channelUID, updateValue);
265
266         assertThat(property.getChannelState().getCache().getChannelState().toString(), is("UPDATE"));
267         verify(connection, times(1)).publish(any(), any(), anyInt(), anyBoolean());
268
269         // Check non writable property
270         property.attributes.settable = false;
271         property.attributesReceived();
272         // Assign old value
273         Value value = property.getChannelState().getCache();
274         Command command = TypeParser.parseCommand(value.getSupportedCommandTypes(), "OLDVALUE");
275         property.getChannelState().getCache().update(command);
276         // Try to update with new value
277         updateValue = new StringType("SOMETHINGNEW");
278         thingHandler.handleCommand(property.channelUID, updateValue);
279         // Expect old value and no MQTT publish
280         assertThat(property.getChannelState().getCache().getChannelState().toString(), is("OLDVALUE"));
281         verify(connection, times(1)).publish(any(), any(), anyInt(), anyBoolean());
282     }
283
284     public Object createSubscriberAnswer(InvocationOnMock invocation) {
285         final AbstractMqttAttributeClass attributes = (AbstractMqttAttributeClass) invocation.getMock();
286         final ScheduledExecutorService scheduler = (ScheduledExecutorService) invocation.getArguments()[0];
287         final Field field = (Field) invocation.getArguments()[1];
288         final String topic = (String) invocation.getArguments()[2];
289         final boolean mandatory = (boolean) invocation.getArguments()[3];
290         final SubscribeFieldToMQTTtopic s = spy(
291                 new SubscribeFieldToMQTTtopic(scheduler, field, attributes, topic, mandatory));
292         doReturn(CompletableFuture.completedFuture(true)).when(s).subscribeAndReceive(any(), anyInt());
293         return s;
294     }
295
296     public Property createSpyProperty(String propertyID, Node node) {
297         // Create a property with the same ID and insert it instead
298         Property property = spy(node.createProperty(propertyID, spy(new PropertyAttributes())));
299         doAnswer(this::createSubscriberAnswer).when(property.attributes).createSubscriber(any(), any(), any(),
300                 anyBoolean());
301         property.attributes.name = "testprop";
302         property.attributes.datatype = DataTypeEnum.string_;
303
304         return property;
305     }
306
307     public Node createSpyNode(String propertyID, Device device) {
308         // Create the node
309         Node node = spy(device.createNode("node", spy(new NodeAttributes())));
310         doReturn(future).when(node.attributes).subscribeAndReceive(any(), any(), anyString(), any(), anyInt());
311         doReturn(future).when(node.attributes).unsubscribe();
312         node.attributes.name = "testnode";
313         node.attributes.properties = new String[] { "property" };
314         doAnswer(this::createSubscriberAnswer).when(node.attributes).createSubscriber(any(), any(), any(),
315                 anyBoolean());
316
317         // Intercept creating a property in the next call and inject a spy'ed property.
318         doAnswer(i -> createSpyProperty("property", node)).when(node).createProperty(any());
319
320         return node;
321     }
322
323     @Test
324     public void propertiesChanged() throws InterruptedException, ExecutionException {
325         thingHandler.device.initialize("homie", "device", new ArrayList<>());
326         ThingHandlerHelper.setConnection(thingHandler, connection);
327
328         // Create mocked homie device tree with one node and one property
329         doAnswer(this::createSubscriberAnswer).when(thingHandler.device.attributes).createSubscriber(any(), any(),
330                 any(), anyBoolean());
331
332         thingHandler.device.attributes.state = ReadyState.ready;
333         thingHandler.device.attributes.name = "device";
334         thingHandler.device.attributes.homie = "3.0";
335         thingHandler.device.attributes.nodes = new String[] { "node" };
336
337         // Intercept creating a node in initialize()->start() and inject a spy'ed node.
338         doAnswer(i -> createSpyNode("node", thingHandler.device)).when(thingHandler.device).createNode(any());
339
340         verify(thingHandler, times(0)).nodeAddedOrChanged(any());
341         verify(thingHandler, times(0)).propertyAddedOrChanged(any());
342
343         thingHandler.initialize();
344
345         assertThat(thingHandler.device.isInitialized(), is(true));
346
347         verify(thingHandler).propertyAddedOrChanged(any());
348         verify(thingHandler).nodeAddedOrChanged(any());
349
350         verify(thingHandler.device).subscribe(any(), any(), anyInt());
351         verify(thingHandler.device).attributesReceived(any(), any(), anyInt());
352
353         assertNotNull(thingHandler.device.nodes.get("node").properties.get("property"));
354
355         assertTrue(thingHandler.delayedProcessing.isArmed());
356
357         // Simulate waiting for the delayed processor
358         thingHandler.delayedProcessing.forceProcessNow();
359
360         // Called for the updated property + for the new channels
361         verify(callback, atLeast(2)).thingUpdated(any());
362
363         final List<@NonNull Channel> channels = thingHandler.getThing().getChannels();
364         assertThat(channels.size(), is(1));
365         assertThat(channels.get(0).getLabel(), is("testprop"));
366         assertThat(channels.get(0).getKind(), is(ChannelKind.STATE));
367
368         final Map<@NonNull String, @NonNull String> properties = thingHandler.getThing().getProperties();
369         assertThat(properties.get(MqttBindingConstants.HOMIE_PROPERTY_VERSION), is("3.0"));
370         assertThat(properties.size(), is(1));
371     }
372 }