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