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