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