2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mqtt.homeassistant.internal.component;
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.*;
21 import java.nio.charset.StandardCharsets;
22 import java.util.List;
23 import java.util.Objects;
25 import java.util.concurrent.CountDownLatch;
26 import java.util.concurrent.TimeUnit;
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;
54 * Abstract class for components tests.
56 * @author Anton Kharuzhy - Initial contribution
59 public abstract class AbstractComponentTests extends AbstractHomeAssistantTests {
60 private static final int SUBSCRIBE_TIMEOUT = 10000;
61 private static final int ATTRIBUTE_RECEIVE_TIMEOUT = 2000;
63 private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
64 private @NonNullByDefault({}) LatchThingHandler thingHandler;
67 public void setupThingHandler() {
68 final var config = haThing.getConfiguration();
70 config.put(HandlerConfiguration.PROPERTY_BASETOPIC, HandlerConfiguration.DEFAULT_BASETOPIC);
71 config.put(HandlerConfiguration.PROPERTY_TOPICS, getConfigTopics());
73 // Plumb thing status updates through
74 doAnswer(invocation -> {
75 ((Thing) invocation.getArgument(0)).setStatusInfo((ThingStatusInfo) invocation.getArgument(1));
77 }).when(callbackMock).statusUpdated(any(Thing.class), any(ThingStatusInfo.class));
79 when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
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);
87 thingHandler.initialize();
91 public void disposeThingHandler() {
92 thingHandler.dispose();
96 * {@link org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents} will wait a config on specified
98 * Topics in config must be without prefix and suffix, they can be converted to full with method
99 * {@link #configTopicToMqtt(String)}
101 * @return config topics
103 protected abstract Set<String> getConfigTopics();
106 * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
108 * @param mqttTopic mqtt topic with configuration
109 * @param json configuration payload in Json
110 * @return discovered component
112 protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
114 return discoverComponent(mqttTopic, json.getBytes(StandardCharsets.UTF_8));
118 * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
120 * @param mqttTopic mqtt topic with configuration
121 * @param jsonPayload configuration payload in Json
122 * @return discovered component
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));
129 assert latch.await(1, TimeUnit.SECONDS);
130 } catch (InterruptedException e) {
131 assertThat(e.getMessage(), false);
133 return Objects.requireNonNull(thingHandler.getDiscoveredComponent());
137 * Assert channel topics, label and value class
139 * @param component component
140 * @param channelId channel
141 * @param stateTopic state topic or empty string
142 * @param commandTopic command topic or empty string
144 * @param valueClass value class
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);
153 * Assert channel topics, label and value class
155 * @param stateChannel channel
156 * @param stateTopic state topic or empty string
157 * @param commandTopic command topic or empty string
159 * @param valueClass value class
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)));
170 * Assert channel state
172 * @param component component
173 * @param channelId channel
174 * @param state expected state
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));
183 assertThat(actualState, is(state));
187 protected void spyOnChannelUpdates(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
189 // It's already thingHandler, but not the spy version
190 component.getChannel(channelId).getState().setChannelStateUpdateListener(thingHandler);
194 * Assert a channel triggers
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));
202 * Assert that given payload was published exact-once on given topic.
204 * @param mqttTopic Mqtt topic
205 * @param payload payload
207 protected void assertPublished(String mqttTopic, String payload) {
208 verify(bridgeConnection).publish(eq(mqttTopic), ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)),
209 anyInt(), anyBoolean());
213 * Assert that given payload was published N times on given topic.
215 * @param mqttTopic Mqtt topic
216 * @param payload payload
217 * @param t payload must be published N times on given topic
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());
225 * Assert that given payload was not published on given topic.
227 * @param mqttTopic Mqtt topic
228 * @param payload payload
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());
236 * Assert that nothing was published on given topic.
238 * @param mqttTopic Mqtt topic
240 protected void assertNothingPublished(String mqttTopic) {
241 verify(bridgeConnection, never()).publish(eq(mqttTopic), any(), anyInt(), anyBoolean());
245 * Publish payload to all subscribers on specified topic.
247 * @param mqttTopic Mqtt topic
248 * @param payload payload
249 * @return true when at least one subscriber found
251 protected boolean publishMessage(String mqttTopic, String payload) {
252 return publishMessage(mqttTopic, payload.getBytes(StandardCharsets.UTF_8));
256 * Publish payload to all subscribers on specified topic.
258 * @param mqttTopic Mqtt topic
259 * @param payload payload
260 * @return true when at least one subscriber found
262 protected boolean publishMessage(String mqttTopic, byte[] payload) {
263 final var topicSubscribers = subscriptions.get(mqttTopic);
265 if (topicSubscribers != null && !topicSubscribers.isEmpty()) {
266 topicSubscribers.forEach(mqttMessageSubscriber -> mqttMessageSubscriber.processMessage(mqttTopic, payload));
273 * Send command to a thing's channel
275 * @param component component
276 * @param channelId channel
277 * @param command command to send
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);
285 protected static class LatchThingHandler extends HomeAssistantThingHandler {
286 private @Nullable CountDownLatch latch;
287 private @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoveredComponent;
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);
298 public void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<@NonNull ?> component) {
299 accept(List.of(component));
300 discoveredComponent = component;
306 public CountDownLatch createWaitForComponentDiscoveredLatch(int count) {
307 final var newLatch = new CountDownLatch(count);
312 public @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> getDiscoveredComponent() {
313 return discoveredComponent;