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