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