]> git.basschouten.com Git - openhab-addons.git/blob
bbb278d530fbfd3fcc0fda6358a3dd96913882bb
[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.homeassistant.internal.component;
14
15 import static org.hamcrest.CoreMatchers.*;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.mockito.ArgumentMatchers.*;
18 import static org.mockito.ArgumentMatchers.any;
19 import static org.mockito.Mockito.*;
20
21 import java.nio.charset.StandardCharsets;
22 import java.util.List;
23 import java.util.Objects;
24 import java.util.Set;
25 import java.util.concurrent.CountDownLatch;
26 import java.util.concurrent.TimeUnit;
27
28 import org.eclipse.jdt.annotation.NonNull;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.junit.jupiter.api.AfterEach;
32 import org.junit.jupiter.api.BeforeEach;
33 import org.mockito.ArgumentMatchers;
34 import org.mockito.Mock;
35 import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
36 import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
37 import org.openhab.binding.mqtt.generic.values.Value;
38 import org.openhab.binding.mqtt.homeassistant.internal.AbstractHomeAssistantTests;
39 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
40 import org.openhab.binding.mqtt.homeassistant.internal.HaID;
41 import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
42 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
43 import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
44 import org.openhab.core.library.types.HSBType;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatusInfo;
47 import org.openhab.core.thing.binding.ThingHandlerCallback;
48 import org.openhab.core.thing.type.AutoUpdatePolicy;
49 import org.openhab.core.thing.type.ChannelTypeRegistry;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.State;
52
53 import com.hubspot.jinjava.Jinjava;
54
55 /**
56  * Abstract class for components tests.
57  *
58  * @author Anton Kharuzhy - Initial contribution
59  */
60 @NonNullByDefault
61 public abstract class AbstractComponentTests extends AbstractHomeAssistantTests {
62     private static final int SUBSCRIBE_TIMEOUT = 10000;
63     private static final int ATTRIBUTE_RECEIVE_TIMEOUT = 2000;
64
65     private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
66     private @NonNullByDefault({}) LatchThingHandler thingHandler;
67
68     @BeforeEach
69     public void setupThingHandler() {
70         final var config = haThing.getConfiguration();
71
72         config.put(HandlerConfiguration.PROPERTY_BASETOPIC, HandlerConfiguration.DEFAULT_BASETOPIC);
73         config.put(HandlerConfiguration.PROPERTY_TOPICS, getConfigTopics());
74
75         // Plumb thing status updates through
76         doAnswer(invocation -> {
77             ((Thing) invocation.getArgument(0)).setStatusInfo((ThingStatusInfo) invocation.getArgument(1));
78             return null;
79         }).when(callbackMock).statusUpdated(any(Thing.class), any(ThingStatusInfo.class));
80
81         when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
82
83         thingHandler = new LatchThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
84                 channelTypeRegistry, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
85         thingHandler.setConnection(bridgeConnection);
86         thingHandler.setCallback(callbackMock);
87         thingHandler = spy(thingHandler);
88
89         thingHandler.initialize();
90     }
91
92     @AfterEach
93     public void disposeThingHandler() {
94         thingHandler.dispose();
95     }
96
97     /**
98      * {@link org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents} will wait a config on specified
99      * topics.
100      * Topics in config must be without prefix and suffix, they can be converted to full with method
101      * {@link #configTopicToMqtt(String)}
102      *
103      * @return config topics
104      */
105     protected abstract Set<String> getConfigTopics();
106
107     /**
108      * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
109      *
110      * @param mqttTopic mqtt topic with configuration
111      * @param json configuration payload in Json
112      * @return discovered component
113      */
114     protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
115             String json) {
116         return discoverComponent(mqttTopic, json.getBytes(StandardCharsets.UTF_8));
117     }
118
119     /**
120      * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
121      *
122      * @param mqttTopic mqtt topic with configuration
123      * @param jsonPayload configuration payload in Json
124      * @return discovered component
125      */
126     protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
127             byte[] jsonPayload) {
128         var latch = thingHandler.createWaitForComponentDiscoveredLatch(1);
129         assertThat(publishMessage(mqttTopic, jsonPayload), is(true));
130         try {
131             assert latch.await(1, TimeUnit.SECONDS);
132         } catch (InterruptedException e) {
133             assertThat(e.getMessage(), false);
134         }
135         return Objects.requireNonNull(thingHandler.getDiscoveredComponent());
136     }
137
138     /**
139      * Assert channel topics, label and value class
140      *
141      * @param component component
142      * @param channelId channel
143      * @param stateTopic state topic or empty string
144      * @param commandTopic command topic or empty string
145      * @param label label
146      * @param valueClass value class
147      */
148     protected static void assertChannel(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
149             String channelId, String stateTopic, String commandTopic, String label, Class<? extends Value> valueClass) {
150         var stateChannel = Objects.requireNonNull(component.getChannel(channelId));
151         assertChannel(stateChannel, stateTopic, commandTopic, label, valueClass);
152     }
153
154     /**
155      * Assert channel topics, label and value class
156      *
157      * @param stateChannel channel
158      * @param stateTopic state topic or empty string
159      * @param commandTopic command topic or empty string
160      * @param label label
161      * @param valueClass value class
162      */
163     protected static void assertChannel(ComponentChannel stateChannel, String stateTopic, String commandTopic,
164             String label, Class<? extends Value> valueClass) {
165         assertThat(stateChannel.getChannel().getLabel(), is(label));
166         assertThat(stateChannel.getState().getStateTopic(), is(stateTopic));
167         assertThat(stateChannel.getState().getCommandTopic(), is(commandTopic));
168         assertThat(stateChannel.getState().getCache(), is(instanceOf(valueClass)));
169     }
170
171     /**
172      * Assert channel topics, label and value class
173      *
174      * @param component component
175      * @param channelId channel
176      * @param stateTopic state topic or empty string
177      * @param commandTopic command topic or empty string
178      * @param label label
179      * @param valueClass value class
180      * @param autoUpdatePolicy Auto Update Policy
181      */
182     protected static void assertChannel(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
183             String channelId, String stateTopic, String commandTopic, String label, Class<? extends Value> valueClass,
184             @Nullable AutoUpdatePolicy autoUpdatePolicy) {
185         var stateChannel = Objects.requireNonNull(component.getChannel(channelId));
186         assertChannel(stateChannel, stateTopic, commandTopic, label, valueClass);
187     }
188
189     /**
190      * Assert channel topics, label and value class
191      *
192      * @param stateChannel channel
193      * @param stateTopic state topic or empty string
194      * @param commandTopic command topic or empty string
195      * @param label label
196      * @param valueClass value class
197      * @param autoUpdatePolicy Auto Update Policy
198      */
199     protected static void assertChannel(ComponentChannel stateChannel, String stateTopic, String commandTopic,
200             String label, Class<? extends Value> valueClass, @Nullable AutoUpdatePolicy autoUpdatePolicy) {
201         assertThat(stateChannel.getChannel().getLabel(), is(label));
202         assertThat(stateChannel.getState().getStateTopic(), is(stateTopic));
203         assertThat(stateChannel.getState().getCommandTopic(), is(commandTopic));
204         assertThat(stateChannel.getState().getCache(), is(instanceOf(valueClass)));
205         assertThat(stateChannel.getChannel().getAutoUpdatePolicy(), is(autoUpdatePolicy));
206     }
207
208     /**
209      * Assert channel state
210      *
211      * @param component component
212      * @param channelId channel
213      * @param state expected state
214      */
215     @SuppressWarnings("null")
216     protected static void assertState(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
217             String channelId, State state) {
218         State actualState = component.getChannel(channelId).getState().getCache().getChannelState();
219         if ((actualState instanceof HSBType actualHsb) && (state instanceof HSBType stateHsb)) {
220             assertThat(actualHsb.closeTo(stateHsb, 0.01), is(true));
221         } else {
222             assertThat(actualState, is(state));
223         }
224     }
225
226     protected void spyOnChannelUpdates(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
227             String channelId) {
228         // It's already thingHandler, but not the spy version
229         component.getChannel(channelId).getState().setChannelStateUpdateListener(thingHandler);
230     }
231
232     /**
233      * Assert a channel triggers
234      */
235     protected void assertTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
236             String channelId, String trigger) {
237         verify(thingHandler).triggerChannel(eq(component.getChannel(channelId).getChannel().getUID()), eq(trigger));
238     }
239
240     /**
241      * Assert that given payload was published exact-once on given topic.
242      *
243      * @param mqttTopic Mqtt topic
244      * @param payload payload
245      */
246     protected void assertPublished(String mqttTopic, String payload) {
247         verify(bridgeConnection).publish(eq(mqttTopic), ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)),
248                 anyInt(), anyBoolean());
249     }
250
251     /**
252      * Assert that given payload was published N times on given topic.
253      *
254      * @param mqttTopic Mqtt topic
255      * @param payload payload
256      * @param t payload must be published N times on given topic
257      */
258     protected void assertPublished(String mqttTopic, String payload, int t) {
259         verify(bridgeConnection, times(t)).publish(eq(mqttTopic),
260                 ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(), anyBoolean());
261     }
262
263     /**
264      * Assert that given payload was not published on given topic.
265      *
266      * @param mqttTopic Mqtt topic
267      * @param payload payload
268      */
269     protected void assertNotPublished(String mqttTopic, String payload) {
270         verify(bridgeConnection, never()).publish(eq(mqttTopic),
271                 ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(), anyBoolean());
272     }
273
274     /**
275      * Assert that nothing was published on given topic.
276      *
277      * @param mqttTopic Mqtt topic
278      */
279     protected void assertNothingPublished(String mqttTopic) {
280         verify(bridgeConnection, never()).publish(eq(mqttTopic), any(), anyInt(), anyBoolean());
281     }
282
283     /**
284      * Publish payload to all subscribers on specified topic.
285      *
286      * @param mqttTopic Mqtt topic
287      * @param payload payload
288      * @return true when at least one subscriber found
289      */
290     protected boolean publishMessage(String mqttTopic, String payload) {
291         return publishMessage(mqttTopic, payload.getBytes(StandardCharsets.UTF_8));
292     }
293
294     /**
295      * Publish payload to all subscribers on specified topic.
296      *
297      * @param mqttTopic Mqtt topic
298      * @param payload payload
299      * @return true when at least one subscriber found
300      */
301     protected boolean publishMessage(String mqttTopic, byte[] payload) {
302         final var topicSubscribers = subscriptions.get(mqttTopic);
303
304         if (topicSubscribers != null && !topicSubscribers.isEmpty()) {
305             topicSubscribers.forEach(mqttMessageSubscriber -> mqttMessageSubscriber.processMessage(mqttTopic, payload));
306             return true;
307         }
308         return false;
309     }
310
311     /**
312      * Send command to a thing's channel
313      *
314      * @param component component
315      * @param channelId channel
316      * @param command command to send
317      */
318     protected void sendCommand(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
319             String channelId, Command command) {
320         var channel = Objects.requireNonNull(component.getChannel(channelId));
321         thingHandler.handleCommand(channel.getChannel().getUID(), command);
322     }
323
324     protected static class LatchThingHandler extends HomeAssistantThingHandler {
325         private @Nullable CountDownLatch latch;
326         private @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoveredComponent;
327
328         public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
329                 MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
330                 int subscribeTimeout, int attributeReceiveTimeout) {
331             super(thing, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry, new Jinjava(),
332                     subscribeTimeout, attributeReceiveTimeout);
333         }
334
335         @Override
336         public void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<@NonNull ?> component) {
337             accept(List.of(component));
338             discoveredComponent = component;
339             if (latch != null) {
340                 latch.countDown();
341             }
342         }
343
344         public CountDownLatch createWaitForComponentDiscoveredLatch(int count) {
345             final var newLatch = new CountDownLatch(count);
346             latch = newLatch;
347             return newLatch;
348         }
349
350         public @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> getDiscoveredComponent() {
351             return discoveredComponent;
352         }
353     }
354 }