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