]> git.basschouten.com Git - openhab-addons.git/blob
2771859417bafd6fd909756f9c9de5b1b6d0d671
[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         if (useNewStyleChannels()) {
84             haThing.setProperty("newStyleChannels", "true");
85         }
86         thingHandler = new LatchThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
87                 channelTypeRegistry, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
88         thingHandler.setConnection(bridgeConnection);
89         thingHandler.setCallback(callbackMock);
90         thingHandler = spy(thingHandler);
91
92         thingHandler.initialize();
93     }
94
95     @AfterEach
96     public void disposeThingHandler() {
97         thingHandler.dispose();
98     }
99
100     /**
101      * {@link org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents} will wait a config on specified
102      * topics.
103      * Topics in config must be without prefix and suffix, they can be converted to full with method
104      * {@link #configTopicToMqtt(String)}
105      *
106      * @return config topics
107      */
108     protected abstract Set<String> getConfigTopics();
109
110     /**
111      * If new style channels should be used for this test.
112      */
113     protected boolean useNewStyleChannels() {
114         return false;
115     }
116
117     /**
118      * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
119      *
120      * @param mqttTopic mqtt topic with configuration
121      * @param json configuration payload in Json
122      * @return discovered component
123      */
124     protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
125             String json) {
126         return discoverComponent(mqttTopic, json.getBytes(StandardCharsets.UTF_8));
127     }
128
129     /**
130      * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
131      *
132      * @param mqttTopic mqtt topic with configuration
133      * @param jsonPayload configuration payload in Json
134      * @return discovered component
135      */
136     protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
137             byte[] jsonPayload) {
138         var latch = thingHandler.createWaitForComponentDiscoveredLatch(1);
139         assertThat(publishMessage(mqttTopic, jsonPayload), is(true));
140         try {
141             assert latch.await(1, TimeUnit.SECONDS);
142         } catch (InterruptedException e) {
143             assertThat(e.getMessage(), false);
144         }
145         return Objects.requireNonNull(thingHandler.getDiscoveredComponent());
146     }
147
148     /**
149      * Assert channel topics, label and value class
150      *
151      * @param component component
152      * @param channelId channel
153      * @param stateTopic state topic or empty string
154      * @param commandTopic command topic or empty string
155      * @param label label
156      * @param valueClass value class
157      */
158     protected static void assertChannel(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
159             String channelId, String stateTopic, String commandTopic, String label, Class<? extends Value> valueClass) {
160         var stateChannel = Objects.requireNonNull(component.getChannel(channelId));
161         assertChannel(stateChannel, stateTopic, commandTopic, label, valueClass);
162     }
163
164     /**
165      * Assert channel topics, label and value class
166      *
167      * @param stateChannel channel
168      * @param stateTopic state topic or empty string
169      * @param commandTopic command topic or empty string
170      * @param label label
171      * @param valueClass value class
172      */
173     protected static void assertChannel(ComponentChannel stateChannel, String stateTopic, String commandTopic,
174             String label, Class<? extends Value> valueClass) {
175         assertThat(stateChannel.getChannel().getLabel(), is(label));
176         assertThat(stateChannel.getState().getStateTopic(), is(stateTopic));
177         assertThat(stateChannel.getState().getCommandTopic(), is(commandTopic));
178         assertThat(stateChannel.getState().getCache(), is(instanceOf(valueClass)));
179     }
180
181     /**
182      * Assert channel topics, label and value class
183      *
184      * @param component component
185      * @param channelId channel
186      * @param stateTopic state topic or empty string
187      * @param commandTopic command topic or empty string
188      * @param label label
189      * @param valueClass value class
190      * @param autoUpdatePolicy Auto Update Policy
191      */
192     protected static void assertChannel(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
193             String channelId, String stateTopic, String commandTopic, String label, Class<? extends Value> valueClass,
194             @Nullable AutoUpdatePolicy autoUpdatePolicy) {
195         var stateChannel = Objects.requireNonNull(component.getChannel(channelId));
196         assertChannel(stateChannel, stateTopic, commandTopic, label, valueClass);
197     }
198
199     /**
200      * Assert channel topics, label and value class
201      *
202      * @param stateChannel channel
203      * @param stateTopic state topic or empty string
204      * @param commandTopic command topic or empty string
205      * @param label label
206      * @param valueClass value class
207      * @param autoUpdatePolicy Auto Update Policy
208      */
209     protected static void assertChannel(ComponentChannel stateChannel, String stateTopic, String commandTopic,
210             String label, Class<? extends Value> valueClass, @Nullable AutoUpdatePolicy autoUpdatePolicy) {
211         assertThat(stateChannel.getChannel().getLabel(), is(label));
212         assertThat(stateChannel.getState().getStateTopic(), is(stateTopic));
213         assertThat(stateChannel.getState().getCommandTopic(), is(commandTopic));
214         assertThat(stateChannel.getState().getCache(), is(instanceOf(valueClass)));
215         assertThat(stateChannel.getChannel().getAutoUpdatePolicy(), is(autoUpdatePolicy));
216     }
217
218     /**
219      * Assert channel state
220      *
221      * @param component component
222      * @param channelId channel
223      * @param state expected state
224      */
225     @SuppressWarnings("null")
226     protected static void assertState(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
227             String channelId, State state) {
228         State actualState = component.getChannel(channelId).getState().getCache().getChannelState();
229         if ((actualState instanceof HSBType actualHsb) && (state instanceof HSBType stateHsb)) {
230             assertThat(actualHsb.closeTo(stateHsb, 0.01), is(true));
231         } else {
232             assertThat(actualState, is(state));
233         }
234     }
235
236     protected void spyOnChannelUpdates(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
237             String channelId) {
238         // It's already thingHandler, but not the spy version
239         component.getChannel(channelId).getState().setChannelStateUpdateListener(thingHandler);
240     }
241
242     /**
243      * Assert a channel triggers
244      */
245     protected void assertTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
246             String channelId, String trigger) {
247         verify(thingHandler).triggerChannel(eq(component.getChannel(channelId).getChannel().getUID()), eq(trigger));
248     }
249
250     /**
251      * Assert a channel does not triggers=
252      */
253     protected void assertNotTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
254             String channelId, String trigger) {
255         verify(thingHandler, never()).triggerChannel(eq(component.getChannel(channelId).getChannel().getUID()),
256                 eq(trigger));
257     }
258
259     /**
260      * Assert that given payload was published exact-once on given topic.
261      *
262      * @param mqttTopic Mqtt topic
263      * @param payload payload
264      */
265     protected void assertPublished(String mqttTopic, String payload) {
266         verify(bridgeConnection).publish(eq(mqttTopic), ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)),
267                 anyInt(), anyBoolean());
268     }
269
270     /**
271      * Assert that given payload was published N times on given topic.
272      *
273      * @param mqttTopic Mqtt topic
274      * @param payload payload
275      * @param t payload must be published N times on given topic
276      */
277     protected void assertPublished(String mqttTopic, String payload, int t) {
278         verify(bridgeConnection, times(t)).publish(eq(mqttTopic),
279                 ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(), anyBoolean());
280     }
281
282     /**
283      * Assert that given payload was not published on given topic.
284      *
285      * @param mqttTopic Mqtt topic
286      * @param payload payload
287      */
288     protected void assertNotPublished(String mqttTopic, String payload) {
289         verify(bridgeConnection, never()).publish(eq(mqttTopic),
290                 ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(), anyBoolean());
291     }
292
293     /**
294      * Assert that nothing was published on given topic.
295      *
296      * @param mqttTopic Mqtt topic
297      */
298     protected void assertNothingPublished(String mqttTopic) {
299         verify(bridgeConnection, never()).publish(eq(mqttTopic), any(), anyInt(), anyBoolean());
300     }
301
302     /**
303      * Publish payload to all subscribers on specified topic.
304      *
305      * @param mqttTopic Mqtt topic
306      * @param payload payload
307      * @return true when at least one subscriber found
308      */
309     protected boolean publishMessage(String mqttTopic, String payload) {
310         return publishMessage(mqttTopic, payload.getBytes(StandardCharsets.UTF_8));
311     }
312
313     /**
314      * Publish payload to all subscribers on specified topic.
315      *
316      * @param mqttTopic Mqtt topic
317      * @param payload payload
318      * @return true when at least one subscriber found
319      */
320     protected boolean publishMessage(String mqttTopic, byte[] payload) {
321         final var topicSubscribers = subscriptions.get(mqttTopic);
322
323         if (topicSubscribers != null && !topicSubscribers.isEmpty()) {
324             topicSubscribers.forEach(mqttMessageSubscriber -> mqttMessageSubscriber.processMessage(mqttTopic, payload));
325             return true;
326         }
327         return false;
328     }
329
330     /**
331      * Send command to a thing's channel
332      *
333      * @param component component
334      * @param channelId channel
335      * @param command command to send
336      */
337     protected void sendCommand(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
338             String channelId, Command command) {
339         var channel = Objects.requireNonNull(component.getChannel(channelId));
340         thingHandler.handleCommand(channel.getChannel().getUID(), command);
341     }
342
343     protected static class LatchThingHandler extends HomeAssistantThingHandler {
344         private @Nullable CountDownLatch latch;
345         private @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoveredComponent;
346
347         public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
348                 MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
349                 int subscribeTimeout, int attributeReceiveTimeout) {
350             super(thing, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry, new Jinjava(),
351                     subscribeTimeout, attributeReceiveTimeout);
352         }
353
354         @Override
355         public void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<@NonNull ?> component) {
356             accept(List.of(component));
357             discoveredComponent = component;
358             if (latch != null) {
359                 latch.countDown();
360             }
361         }
362
363         public CountDownLatch createWaitForComponentDiscoveredLatch(int count) {
364             final var newLatch = new CountDownLatch(count);
365             latch = newLatch;
366             return newLatch;
367         }
368
369         public @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> getDiscoveredComponent() {
370             return discoveredComponent;
371         }
372     }
373 }