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