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