]> git.basschouten.com Git - openhab-addons.git/blob
1484868e83a0a9e0043215af89baf73d978bd191
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.instanceOf;
16 import static org.hamcrest.CoreMatchers.is;
17 import static org.hamcrest.MatcherAssert.assertThat;
18 import static org.mockito.ArgumentMatchers.anyBoolean;
19 import static org.mockito.ArgumentMatchers.anyInt;
20 import static org.mockito.ArgumentMatchers.eq;
21 import static org.mockito.Mockito.never;
22 import static org.mockito.Mockito.spy;
23 import static org.mockito.Mockito.times;
24 import static org.mockito.Mockito.verify;
25 import static org.mockito.Mockito.when;
26
27 import java.nio.charset.StandardCharsets;
28 import java.util.List;
29 import java.util.Objects;
30 import java.util.Set;
31 import java.util.concurrent.CountDownLatch;
32 import java.util.concurrent.TimeUnit;
33
34 import org.eclipse.jdt.annotation.NonNull;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.junit.jupiter.api.AfterEach;
38 import org.junit.jupiter.api.BeforeEach;
39 import org.mockito.Mock;
40 import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
41 import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
42 import org.openhab.binding.mqtt.generic.values.Value;
43 import org.openhab.binding.mqtt.homeassistant.internal.AbstractHomeAssistantTests;
44 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
45 import org.openhab.binding.mqtt.homeassistant.internal.HaID;
46 import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
47 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
48 import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
49 import org.openhab.core.thing.Thing;
50 import org.openhab.core.thing.binding.ThingHandlerCallback;
51 import org.openhab.core.types.State;
52
53 /**
54  * Abstract class for components tests.
55  *
56  * @author Anton Kharuzhy - Initial contribution
57  */
58 @SuppressWarnings({ "ConstantConditions" })
59 @NonNullByDefault
60 public abstract class AbstractComponentTests extends AbstractHomeAssistantTests {
61     private static final int SUBSCRIBE_TIMEOUT = 10000;
62     private static final int ATTRIBUTE_RECEIVE_TIMEOUT = 2000;
63
64     private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
65     private @NonNullByDefault({}) LatchThingHandler thingHandler;
66
67     @BeforeEach
68     public void setupThingHandler() {
69         final var config = haThing.getConfiguration();
70
71         config.put(HandlerConfiguration.PROPERTY_BASETOPIC, HandlerConfiguration.DEFAULT_BASETOPIC);
72         config.put(HandlerConfiguration.PROPERTY_TOPICS, getConfigTopics());
73
74         when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
75
76         thingHandler = new LatchThingHandler(haThing, channelTypeProvider, transformationServiceProvider,
77                 SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
78         thingHandler.setConnection(bridgeConnection);
79         thingHandler.setCallback(callbackMock);
80         thingHandler = spy(thingHandler);
81
82         thingHandler.initialize();
83     }
84
85     @AfterEach
86     public void disposeThingHandler() {
87         thingHandler.dispose();
88     }
89
90     /**
91      * {@link org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents} will wait a config on specified
92      * topics.
93      * Topics in config must be without prefix and suffix, they can be converted to full with method
94      * {@link #configTopicToMqtt(String)}
95      *
96      * @return config topics
97      */
98     protected abstract Set<String> getConfigTopics();
99
100     /**
101      * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
102      *
103      * @param mqttTopic mqtt topic with configuration
104      * @param json configuration payload in Json
105      * @return discovered component
106      */
107     protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
108             String json) {
109         return discoverComponent(mqttTopic, json.getBytes(StandardCharsets.UTF_8));
110     }
111
112     /**
113      * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
114      *
115      * @param mqttTopic mqtt topic with configuration
116      * @param jsonPayload configuration payload in Json
117      * @return discovered component
118      */
119     protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
120             byte[] jsonPayload) {
121         var latch = thingHandler.createWaitForComponentDiscoveredLatch(1);
122         assertThat(publishMessage(mqttTopic, jsonPayload), is(true));
123         try {
124             assert latch.await(1, TimeUnit.SECONDS);
125         } catch (InterruptedException e) {
126             assertThat(e.getMessage(), false);
127         }
128         return Objects.requireNonNull(thingHandler.getDiscoveredComponent());
129     }
130
131     /**
132      * Assert channel topics, label and value class
133      *
134      * @param component component
135      * @param channelId channel
136      * @param stateTopic state topic or empty string
137      * @param commandTopic command topic or empty string
138      * @param label label
139      * @param valueClass value class
140      */
141     protected static void assertChannel(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
142             String channelId, String stateTopic, String commandTopic, String label, Class<? extends Value> valueClass) {
143         var stateChannel = Objects.requireNonNull(component.getChannel(channelId));
144         assertChannel(stateChannel, stateTopic, commandTopic, label, valueClass);
145     }
146
147     /**
148      * Assert channel topics, label and value class
149      *
150      * @param stateChannel channel
151      * @param stateTopic state topic or empty string
152      * @param commandTopic command topic or empty string
153      * @param label label
154      * @param valueClass value class
155      */
156     protected static void assertChannel(ComponentChannel stateChannel, String stateTopic, String commandTopic,
157             String label, Class<? extends Value> valueClass) {
158         assertThat(stateChannel.getChannel().getLabel(), is(label));
159         assertThat(stateChannel.getState().getStateTopic(), is(stateTopic));
160         assertThat(stateChannel.getState().getCommandTopic(), is(commandTopic));
161         assertThat(stateChannel.getState().getCache(), is(instanceOf(valueClass)));
162     }
163
164     /**
165      * Assert channel state
166      *
167      * @param component component
168      * @param channelId channel
169      * @param state expected state
170      */
171     protected static void assertState(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
172             String channelId, State state) {
173         assertThat(component.getChannel(channelId).getState().getCache().getChannelState(), is(state));
174     }
175
176     /**
177      * Assert that given payload was published exact-once on given topic.
178      *
179      * @param mqttTopic Mqtt topic
180      * @param payload payload
181      */
182     protected void assertPublished(String mqttTopic, String payload) {
183         verify(bridgeConnection).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(),
184                 anyBoolean());
185     }
186
187     /**
188      * Assert that given payload was published N times on given topic.
189      *
190      * @param mqttTopic Mqtt topic
191      * @param payload payload
192      * @param t payload must be published N times on given topic
193      */
194     protected void assertPublished(String mqttTopic, String payload, int t) {
195         verify(bridgeConnection, times(t)).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)),
196                 anyInt(), anyBoolean());
197     }
198
199     /**
200      * Assert that given payload was not published on given topic.
201      *
202      * @param mqttTopic Mqtt topic
203      * @param payload payload
204      */
205     protected void assertNotPublished(String mqttTopic, String payload) {
206         verify(bridgeConnection, never()).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(),
207                 anyBoolean());
208     }
209
210     /**
211      * Publish payload to all subscribers on specified topic.
212      *
213      * @param mqttTopic Mqtt topic
214      * @param payload payload
215      * @return true when at least one subscriber found
216      */
217     protected boolean publishMessage(String mqttTopic, String payload) {
218         return publishMessage(mqttTopic, payload.getBytes(StandardCharsets.UTF_8));
219     }
220
221     /**
222      * Publish payload to all subscribers on specified topic.
223      *
224      * @param mqttTopic Mqtt topic
225      * @param payload payload
226      * @return true when at least one subscriber found
227      */
228     protected boolean publishMessage(String mqttTopic, byte[] payload) {
229         final var topicSubscribers = subscriptions.get(mqttTopic);
230
231         if (topicSubscribers != null && !topicSubscribers.isEmpty()) {
232             topicSubscribers.forEach(mqttMessageSubscriber -> mqttMessageSubscriber.processMessage(mqttTopic, payload));
233             return true;
234         }
235         return false;
236     }
237
238     protected static class LatchThingHandler extends HomeAssistantThingHandler {
239         private @Nullable CountDownLatch latch;
240         private @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoveredComponent;
241
242         public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
243                 TransformationServiceProvider transformationServiceProvider, int subscribeTimeout,
244                 int attributeReceiveTimeout) {
245             super(thing, channelTypeProvider, transformationServiceProvider, subscribeTimeout, attributeReceiveTimeout);
246         }
247
248         @Override
249         public void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<@NonNull ?> component) {
250             accept(List.of(component));
251             discoveredComponent = component;
252             if (latch != null) {
253                 latch.countDown();
254             }
255         }
256
257         public CountDownLatch createWaitForComponentDiscoveredLatch(int count) {
258             final var newLatch = new CountDownLatch(count);
259             latch = newLatch;
260             return newLatch;
261         }
262
263         public @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> getDiscoveredComponent() {
264             return discoveredComponent;
265         }
266     }
267 }