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.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.AutoUpdatePolicy;
49 import org.openhab.core.thing.type.ChannelTypeRegistry;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.State;
53 import com.hubspot.jinjava.Jinjava;
56 * Abstract class for components tests.
58 * @author Anton Kharuzhy - Initial contribution
61 public abstract class AbstractComponentTests extends AbstractHomeAssistantTests {
62 private static final int SUBSCRIBE_TIMEOUT = 10000;
63 private static final int ATTRIBUTE_RECEIVE_TIMEOUT = 2000;
65 private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
66 private @NonNullByDefault({}) LatchThingHandler thingHandler;
69 public void setupThingHandler() {
70 final var config = haThing.getConfiguration();
72 config.put(HandlerConfiguration.PROPERTY_BASETOPIC, HandlerConfiguration.DEFAULT_BASETOPIC);
73 config.put(HandlerConfiguration.PROPERTY_TOPICS, getConfigTopics());
75 // Plumb thing status updates through
76 doAnswer(invocation -> {
77 ((Thing) invocation.getArgument(0)).setStatusInfo((ThingStatusInfo) invocation.getArgument(1));
79 }).when(callbackMock).statusUpdated(any(Thing.class), any(ThingStatusInfo.class));
81 when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
83 if (useNewStyleChannels()) {
84 haThing.setProperty("newStyleChannels", "true");
86 thingHandler = new LatchThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
87 channelTypeRegistry, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
88 thingHandler.setConnection(bridgeConnection);
89 thingHandler.setCallback(callbackMock);
90 thingHandler = spy(thingHandler);
92 thingHandler.initialize();
96 public void disposeThingHandler() {
97 thingHandler.dispose();
101 * {@link org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents} will wait a config on specified
103 * Topics in config must be without prefix and suffix, they can be converted to full with method
104 * {@link #configTopicToMqtt(String)}
106 * @return config topics
108 protected abstract Set<String> getConfigTopics();
111 * If new style channels should be used for this test.
113 protected boolean useNewStyleChannels() {
118 * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
120 * @param mqttTopic mqtt topic with configuration
121 * @param json configuration payload in Json
122 * @return discovered component
124 protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
126 return discoverComponent(mqttTopic, json.getBytes(StandardCharsets.UTF_8));
130 * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
132 * @param mqttTopic mqtt topic with configuration
133 * @param jsonPayload configuration payload in Json
134 * @return discovered component
136 protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
137 byte[] jsonPayload) {
138 var latch = thingHandler.createWaitForComponentDiscoveredLatch(1);
139 assertThat(publishMessage(mqttTopic, jsonPayload), is(true));
141 assert latch.await(1, TimeUnit.SECONDS);
142 } catch (InterruptedException e) {
143 assertThat(e.getMessage(), false);
145 return Objects.requireNonNull(thingHandler.getDiscoveredComponent());
149 * Assert channel topics, label and value class
151 * @param component component
152 * @param channelId channel
153 * @param stateTopic state topic or empty string
154 * @param commandTopic command topic or empty string
156 * @param valueClass value class
158 protected static void assertChannel(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
159 String channelId, String stateTopic, String commandTopic, String label, Class<? extends Value> valueClass) {
160 var stateChannel = Objects.requireNonNull(component.getChannel(channelId));
161 assertChannel(stateChannel, stateTopic, commandTopic, label, valueClass);
165 * Assert channel topics, label and value class
167 * @param stateChannel channel
168 * @param stateTopic state topic or empty string
169 * @param commandTopic command topic or empty string
171 * @param valueClass value class
173 protected static void assertChannel(ComponentChannel stateChannel, String stateTopic, String commandTopic,
174 String label, Class<? extends Value> valueClass) {
175 assertThat(stateChannel.getChannel().getLabel(), is(label));
176 assertThat(stateChannel.getState().getStateTopic(), is(stateTopic));
177 assertThat(stateChannel.getState().getCommandTopic(), is(commandTopic));
178 assertThat(stateChannel.getState().getCache(), is(instanceOf(valueClass)));
182 * Assert channel topics, label and value class
184 * @param component component
185 * @param channelId channel
186 * @param stateTopic state topic or empty string
187 * @param commandTopic command topic or empty string
189 * @param valueClass value class
190 * @param autoUpdatePolicy Auto Update Policy
192 protected static void assertChannel(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
193 String channelId, String stateTopic, String commandTopic, String label, Class<? extends Value> valueClass,
194 @Nullable AutoUpdatePolicy autoUpdatePolicy) {
195 var stateChannel = Objects.requireNonNull(component.getChannel(channelId));
196 assertChannel(stateChannel, stateTopic, commandTopic, label, valueClass);
200 * Assert channel topics, label and value class
202 * @param stateChannel channel
203 * @param stateTopic state topic or empty string
204 * @param commandTopic command topic or empty string
206 * @param valueClass value class
207 * @param autoUpdatePolicy Auto Update Policy
209 protected static void assertChannel(ComponentChannel stateChannel, String stateTopic, String commandTopic,
210 String label, Class<? extends Value> valueClass, @Nullable AutoUpdatePolicy autoUpdatePolicy) {
211 assertThat(stateChannel.getChannel().getLabel(), is(label));
212 assertThat(stateChannel.getState().getStateTopic(), is(stateTopic));
213 assertThat(stateChannel.getState().getCommandTopic(), is(commandTopic));
214 assertThat(stateChannel.getState().getCache(), is(instanceOf(valueClass)));
215 assertThat(stateChannel.getChannel().getAutoUpdatePolicy(), is(autoUpdatePolicy));
219 * Assert channel state
221 * @param component component
222 * @param channelId channel
223 * @param state expected state
225 @SuppressWarnings("null")
226 protected static void assertState(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
227 String channelId, State state) {
228 State actualState = component.getChannel(channelId).getState().getCache().getChannelState();
229 if ((actualState instanceof HSBType actualHsb) && (state instanceof HSBType stateHsb)) {
230 assertThat(actualHsb.closeTo(stateHsb, 0.01), is(true));
232 assertThat(actualState, is(state));
236 protected void spyOnChannelUpdates(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
238 // It's already thingHandler, but not the spy version
239 component.getChannel(channelId).getState().setChannelStateUpdateListener(thingHandler);
243 * Assert a channel triggers
245 protected void assertTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
246 String channelId, String trigger) {
247 verify(thingHandler).triggerChannel(eq(component.getChannel(channelId).getChannel().getUID()), eq(trigger));
251 * Assert a channel does not triggers=
253 protected void assertNotTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
254 String channelId, String trigger) {
255 verify(thingHandler, never()).triggerChannel(eq(component.getChannel(channelId).getChannel().getUID()),
260 * Assert that given payload was published exact-once on given topic.
262 * @param mqttTopic Mqtt topic
263 * @param payload payload
265 protected void assertPublished(String mqttTopic, String payload) {
266 verify(bridgeConnection).publish(eq(mqttTopic), ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)),
267 anyInt(), anyBoolean());
271 * Assert that given payload was published N times on given topic.
273 * @param mqttTopic Mqtt topic
274 * @param payload payload
275 * @param t payload must be published N times on given topic
277 protected void assertPublished(String mqttTopic, String payload, int t) {
278 verify(bridgeConnection, times(t)).publish(eq(mqttTopic),
279 ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(), anyBoolean());
283 * Assert that given payload was not published on given topic.
285 * @param mqttTopic Mqtt topic
286 * @param payload payload
288 protected void assertNotPublished(String mqttTopic, String payload) {
289 verify(bridgeConnection, never()).publish(eq(mqttTopic),
290 ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(), anyBoolean());
294 * Assert that nothing was published on given topic.
296 * @param mqttTopic Mqtt topic
298 protected void assertNothingPublished(String mqttTopic) {
299 verify(bridgeConnection, never()).publish(eq(mqttTopic), any(), anyInt(), anyBoolean());
303 * Publish payload to all subscribers on specified topic.
305 * @param mqttTopic Mqtt topic
306 * @param payload payload
307 * @return true when at least one subscriber found
309 protected boolean publishMessage(String mqttTopic, String payload) {
310 return publishMessage(mqttTopic, payload.getBytes(StandardCharsets.UTF_8));
314 * Publish payload to all subscribers on specified topic.
316 * @param mqttTopic Mqtt topic
317 * @param payload payload
318 * @return true when at least one subscriber found
320 protected boolean publishMessage(String mqttTopic, byte[] payload) {
321 final var topicSubscribers = subscriptions.get(mqttTopic);
323 if (topicSubscribers != null && !topicSubscribers.isEmpty()) {
324 topicSubscribers.forEach(mqttMessageSubscriber -> mqttMessageSubscriber.processMessage(mqttTopic, payload));
331 * Send command to a thing's channel
333 * @param component component
334 * @param channelId channel
335 * @param command command to send
337 protected void sendCommand(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
338 String channelId, Command command) {
339 var channel = Objects.requireNonNull(component.getChannel(channelId));
340 thingHandler.handleCommand(channel.getChannel().getUID(), command);
343 protected static class LatchThingHandler extends HomeAssistantThingHandler {
344 private @Nullable CountDownLatch latch;
345 private @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoveredComponent;
347 public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
348 MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
349 int subscribeTimeout, int attributeReceiveTimeout) {
350 super(thing, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry, new Jinjava(),
351 subscribeTimeout, attributeReceiveTimeout);
355 public void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<@NonNull ?> component) {
356 accept(List.of(component));
357 discoveredComponent = component;
363 public CountDownLatch createWaitForComponentDiscoveredLatch(int count) {
364 final var newLatch = new CountDownLatch(count);
369 public @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> getDiscoveredComponent() {
370 return discoveredComponent;