From: Wouter Born Date: Tue, 26 Apr 2022 18:24:11 +0000 (+0200) Subject: [mqtt] Revive disabled itests (#12431) X-Git-Url: https://git.basschouten.com/?a=commitdiff_plain;h=62c30d034f15b162392524ec4ed3c8496377bebf;p=openhab-addons.git [mqtt] Revive disabled itests (#12431) This fixes all the compilation/dependency issues in the MQTT itests so they can be reenabled again. The tests now create and use their own Moquette instance instead of the removed embedded MQTT broker. The moquette-broker JAR is also included in the test bundles as workaround for its missing OSGi bundle manifest headers. Signed-off-by: Wouter Born --- diff --git a/.github/scripts/maven-build b/.github/scripts/maven-build index 7772281102..6a3a2f7c0a 100755 --- a/.github/scripts/maven-build +++ b/.github/scripts/maven-build @@ -66,7 +66,7 @@ function addon_projects() { local addon="$1" # include add-on projects - local projects=":$(find . -mindepth 2 -maxdepth 2 -type d -regextype egrep -regex "./(bundles|itests)/$addon(\..*)?$" | grep -Ev 'org.openhab.binding.mqtt.homeassistant.tests|org.openhab.binding.mqtt.homie.tests' | sort | sed -E 's#./(bundles|itests)/##g' | xargs | sed 's# #,:#g')" + local projects=":$(find . -mindepth 2 -maxdepth 2 -type d -regextype egrep -regex "./(bundles|itests)/$addon(\..*)?$" | sort | sed -E 's#./(bundles|itests)/##g' | xargs | sed 's# #,:#g')" # include BOMs projects="$projects,:org.openhab.addons.bom.openhab-core-index,:org.openhab.addons.bom.runtime-index,:org.openhab.addons.bom.test-index" diff --git a/itests/itest-common.bndrun b/itests/itest-common.bndrun index 6ff2541ba5..babdeb6db7 100644 --- a/itests/itest-common.bndrun +++ b/itests/itest-common.bndrun @@ -11,9 +11,6 @@ # Run all integration tests which are named xyzTest Test-Cases: ${classes;CONCRETE;PUBLIC;NAMED;*Test} -# A temporary inclusion until an R7 framework is available -Import-Package: org.osgi.framework.*;version="[1.8,2)",* - # We would like to use the slf4j-api and implementation provided by pax-logging -runblacklist.itest-common: \ bnd.identity;id='slf4j.api' diff --git a/itests/org.openhab.binding.mqtt.homeassistant.tests/itest.bndrun b/itests/org.openhab.binding.mqtt.homeassistant.tests/itest.bndrun index 0d60a1ca8a..43ddec015c 100644 --- a/itests/org.openhab.binding.mqtt.homeassistant.tests/itest.bndrun +++ b/itests/org.openhab.binding.mqtt.homeassistant.tests/itest.bndrun @@ -3,92 +3,110 @@ Bundle-SymbolicName: ${project.artifactId} Fragment-Host: org.openhab.binding.mqtt.homeassistant +Import-Package: \ + com.bugsnag.*;resolution:=optional,\ + com.librato.metrics.reporter.*;resolution:=optional,\ + * + +-includeresource: \ + moquette-broker-[0-9.]*.jar;lib:=true + -runrequires: \ bnd.identity;id='org.openhab.binding.mqtt.homeassistant.tests',\ bnd.identity;id='org.openhab.core.binding.xml',\ - bnd.identity;id='org.openhab.core.thing.xml',\ - bnd.identity;id='org.openhab.io.mqttembeddedbroker' + bnd.identity;id='org.openhab.core.thing.xml' # We would like to use the "volatile" storage only -runblacklist: \ bnd.identity;id='org.openhab.core.storage.json' --runvm: \ +-runvm.mqtt: \ -Dio.netty.noUnsafe=true,\ - -Dmqttembeddedbroker.port=${mqttembeddedbroker.port} + -Dmqttbroker.port=${mqttbroker.port} # # done # -runbundles: \ - ch.qos.logback.core;version='[1.2.3,1.2.4)',\ - com.google.gson;version='[2.8.2,2.8.3)',\ - javax.measure.unit-api;version='[1.0.0,1.0.1)',\ - org.apache.commons.lang;version='[2.6.0,2.6.1)',\ - org.apache.felix.configadmin;version='[1.9.8,1.9.9)',\ org.apache.felix.http.servlet-api;version='[1.1.2,1.1.3)',\ - org.apache.felix.scr;version='[2.1.10,2.1.11)',\ - org.apache.servicemix.bundles.xstream;version='[1.4.7,1.4.8)',\ org.eclipse.equinox.event;version='[1.4.300,1.4.301)',\ - org.objenesis;version='[2.6.0,2.6.1)',\ org.osgi.service.event;version='[1.4.0,1.4.1)',\ - slf4j.api;version='[1.7.25,1.7.26)',\ - com.h2database.mvstore;version='[1.4.199,1.4.200)',\ - io.netty.buffer;version='[4.1.42,4.1.43)',\ - io.netty.codec;version='[4.1.42,4.1.43)',\ - io.netty.codec-mqtt;version='[4.1.42,4.1.43)',\ - io.netty.common;version='[4.1.42,4.1.43)',\ - io.netty.handler;version='[4.1.42,4.1.43)',\ - io.netty.resolver;version='[4.1.42,4.1.43)',\ - io.netty.transport;version='[4.1.42,4.1.43)',\ - tec.uom.lib.uom-lib-common;version='[1.0.3,1.0.4)',\ - tec.uom.se;version='[1.0.10,1.0.11)',\ - ch.qos.logback.classic;version='[1.2.3,1.2.4)',\ - biz.aQute.tester.junit-platform;version='[5.1.2,5.1.3)',\ - com.google.dagger;version='[2.20.0,2.20.1)',\ - com.hivemq.client.mqtt;version='[1.1.2,1.1.3)',\ - io.netty.codec-http;version='[4.1.34,4.1.35)',\ - io.netty.transport-native-epoll;version='[4.1.34,4.1.35)',\ - io.netty.transport-native-unix-common;version='[4.1.34,4.1.35)',\ - io.reactivex.rxjava2.rxjava;version='[2.2.5,2.2.6)',\ - junit-jupiter-api;version='[5.6.2,5.6.3)',\ - junit-jupiter-engine;version='[5.6.2,5.6.3)',\ - junit-platform-commons;version='[1.6.2,1.6.3)',\ - junit-platform-engine;version='[1.6.2,1.6.3)',\ - junit-platform-launcher;version='[1.6.2,1.6.3)',\ - net.bytebuddy.byte-buddy;version='[1.10.13,1.10.14)',\ - net.bytebuddy.byte-buddy-agent;version='[1.10.13,1.10.14)',\ - org.apache.aries.javax.jax.rs-api;version='[1.0.0,1.0.1)',\ - org.apache.commons.codec;version='[1.10.0,1.10.1)',\ - org.eclipse.jetty.http;version='[9.4.20,9.4.21)',\ - org.eclipse.jetty.io;version='[9.4.20,9.4.21)',\ - org.eclipse.jetty.security;version='[9.4.20,9.4.21)',\ - org.eclipse.jetty.server;version='[9.4.20,9.4.21)',\ - org.eclipse.jetty.servlet;version='[9.4.20,9.4.21)',\ - org.eclipse.jetty.util;version='[9.4.20,9.4.21)',\ org.glassfish.hk2.external.javax.inject;version='[2.4.0,2.4.1)',\ org.hamcrest;version='[2.2.0,2.2.1)',\ org.jctools.core;version='[2.1.2,2.1.3)',\ - org.mockito.mockito-core;version='[3.4.6,3.4.7)',\ - org.openhab.binding.mqtt;version='[3.0.0,3.0.1)',\ - org.openhab.binding.mqtt.generic;version='[3.0.0,3.0.1)',\ - org.openhab.binding.mqtt.homeassistant;version='[3.0.0,3.0.1)',\ - org.openhab.binding.mqtt.homeassistant.tests;version='[3.0.0,3.0.1)',\ - org.openhab.core;version='[3.0.0,3.0.1)',\ - org.openhab.core.binding.xml;version='[3.0.0,3.0.1)',\ - org.openhab.core.config.core;version='[3.0.0,3.0.1)',\ - org.openhab.core.config.discovery;version='[3.0.0,3.0.1)',\ - org.openhab.core.config.xml;version='[3.0.0,3.0.1)',\ - org.openhab.core.io.console;version='[3.0.0,3.0.1)',\ - org.openhab.core.io.transport.mqtt;version='[3.0.0,3.0.1)',\ - org.openhab.core.test;version='[3.0.0,3.0.1)',\ - org.openhab.core.thing;version='[3.0.0,3.0.1)',\ - org.openhab.core.thing.xml;version='[3.0.0,3.0.1)',\ - org.openhab.core.transform;version='[3.0.0,3.0.1)',\ - org.openhab.io.mqttembeddedbroker;version='[3.0.0,3.0.1)',\ org.opentest4j;version='[1.2.0,1.2.1)',\ - org.reactivestreams.reactive-streams;version='[1.0.2,1.0.3)',\ jakarta.xml.bind-api;version='[2.3.3,2.3.4)',\ com.sun.xml.bind.jaxb-osgi;version='[2.3.3,2.3.4)',\ - org.glassfish.hk2.osgi-resource-locator;version='[1.0.1,1.0.2)',\ - org.apache.servicemix.specs.activation-api-1.2.1;version='[1.2.1,1.2.2)' + org.apache.servicemix.specs.activation-api-1.2.1;version='[1.2.1,1.2.2)',\ + com.google.dagger;version='[2.27.0,2.27.1)',\ + com.google.gson;version='[2.8.9,2.8.10)',\ + com.hivemq.client.mqtt;version='[1.2.2,1.2.3)',\ + io.netty.buffer;version='[4.1.72,4.1.73)',\ + io.netty.codec;version='[4.1.72,4.1.73)',\ + io.netty.codec-http;version='[4.1.59,4.1.60)',\ + io.netty.codec-socks;version='[4.1.72,4.1.73)',\ + io.netty.common;version='[4.1.72,4.1.73)',\ + io.netty.handler;version='[4.1.72,4.1.73)',\ + io.netty.handler-proxy;version='[4.1.72,4.1.73)',\ + io.netty.resolver;version='[4.1.72,4.1.73)',\ + io.netty.tcnative-classes;version='[2.0.46,2.0.47)',\ + io.netty.transport;version='[4.1.72,4.1.73)',\ + io.netty.transport-native-epoll;version='[4.1.59,4.1.60)',\ + io.netty.transport-native-unix-common;version='[4.1.59,4.1.60)',\ + io.reactivex.rxjava2.rxjava;version='[2.2.19,2.2.20)',\ + jakarta.annotation-api;version='[2.0.0,2.0.1)',\ + jakarta.inject.jakarta.inject-api;version='[2.0.0,2.0.1)',\ + javax.measure.unit-api;version='[2.1.2,2.1.3)',\ + junit-jupiter-api;version='[5.8.1,5.8.2)',\ + junit-jupiter-engine;version='[5.8.1,5.8.2)',\ + junit-platform-commons;version='[1.8.1,1.8.2)',\ + junit-platform-engine;version='[1.8.1,1.8.2)',\ + junit-platform-launcher;version='[1.8.1,1.8.2)',\ + net.bytebuddy.byte-buddy;version='[1.12.1,1.12.2)',\ + net.bytebuddy.byte-buddy-agent;version='[1.12.1,1.12.2)',\ + org.apache.aries.javax.jax.rs-api;version='[1.0.1,1.0.2)',\ + org.apache.felix.configadmin;version='[1.9.22,1.9.23)',\ + org.apache.felix.scr;version='[2.1.30,2.1.31)',\ + org.eclipse.jetty.http;version='[9.4.43,9.4.44)',\ + org.eclipse.jetty.io;version='[9.4.43,9.4.44)',\ + org.eclipse.jetty.security;version='[9.4.43,9.4.44)',\ + org.eclipse.jetty.server;version='[9.4.43,9.4.44)',\ + org.eclipse.jetty.servlet;version='[9.4.43,9.4.44)',\ + org.eclipse.jetty.util;version='[9.4.43,9.4.44)',\ + org.eclipse.jetty.util.ajax;version='[9.4.43,9.4.44)',\ + org.glassfish.hk2.osgi-resource-locator;version='[1.0.3,1.0.4)',\ + org.jsr-305;version='[3.0.2,3.0.3)',\ + org.mockito.junit-jupiter;version='[4.1.0,4.1.1)',\ + org.mockito.mockito-core;version='[4.1.0,4.1.1)',\ + org.objenesis;version='[3.2.0,3.2.1)',\ + org.openhab.binding.mqtt;version='[3.3.0,3.3.1)',\ + org.openhab.binding.mqtt.generic;version='[3.3.0,3.3.1)',\ + org.openhab.binding.mqtt.homeassistant;version='[3.3.0,3.3.1)',\ + org.openhab.binding.mqtt.homeassistant.tests;version='[3.3.0,3.3.1)',\ + org.openhab.core;version='[3.3.0,3.3.1)',\ + org.openhab.core.binding.xml;version='[3.3.0,3.3.1)',\ + org.openhab.core.config.core;version='[3.3.0,3.3.1)',\ + org.openhab.core.config.discovery;version='[3.3.0,3.3.1)',\ + org.openhab.core.config.xml;version='[3.3.0,3.3.1)',\ + org.openhab.core.io.console;version='[3.3.0,3.3.1)',\ + org.openhab.core.io.transport.mqtt;version='[3.3.0,3.3.1)',\ + org.openhab.core.test;version='[3.3.0,3.3.1)',\ + org.openhab.core.thing;version='[3.3.0,3.3.1)',\ + org.openhab.core.thing.xml;version='[3.3.0,3.3.1)',\ + org.openhab.core.transform;version='[3.3.0,3.3.1)',\ + org.ops4j.pax.logging.pax-logging-api;version='[2.0.14,2.0.15)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)',\ + org.osgi.util.function;version='[1.2.0,1.2.1)',\ + org.osgi.util.promise;version='[1.2.0,1.2.1)',\ + org.reactivestreams.reactive-streams;version='[1.0.3,1.0.4)',\ + si-units;version='[2.1.0,2.1.1)',\ + si.uom.si-quantity;version='[2.1.0,2.1.1)',\ + tech.units.indriya;version='[2.1.2,2.1.3)',\ + uom-lib-common;version='[2.1.0,2.1.1)',\ + xstream;version='[1.4.19,1.4.20)',\ + com.h2database.mvstore;version='[1.4.199,1.4.200)',\ + com.zaxxer.HikariCP;version='[2.4.7,2.4.8)',\ + io.dropwizard.metrics.core;version='[3.2.2,3.2.3)',\ + io.netty.codec-mqtt;version='[4.1.72,4.1.73)',\ + org.apache.commons.codec;version='[1.10.0,1.10.1)',\ + biz.aQute.tester.junit-platform;version='[6.2.0,6.2.1)' diff --git a/itests/org.openhab.binding.mqtt.homeassistant.tests/pom.xml b/itests/org.openhab.binding.mqtt.homeassistant.tests/pom.xml index b60da70c66..071b7b0a0a 100644 --- a/itests/org.openhab.binding.mqtt.homeassistant.tests/pom.xml +++ b/itests/org.openhab.binding.mqtt.homeassistant.tests/pom.xml @@ -7,13 +7,17 @@ org.openhab.addons.itests org.openhab.addons.reactor.itests - 3.1.0-SNAPSHOT + 3.3.0-SNAPSHOT org.openhab.binding.mqtt.homeassistant.tests openHAB Add-ons :: Integration Tests :: MQTT HomeAssistant Tests + + 1883 + + org.openhab.addons.bundles @@ -31,52 +35,43 @@ ${project.version} - com.github.j-n-k + com.h2database + h2-mvstore + 1.4.199 + + + io.moquette moquette-broker - 0.13.0.OH2 - - - org.slf4j - slf4j-api - - - org.slf4j - slf4j-log4j12 - - - org.mockito - mockito-core - - + 0.15 io.netty - netty-common + netty-buffer ${netty.version} io.netty - netty-buffer + netty-codec ${netty.version} io.netty - netty-transport + netty-codec-mqtt ${netty.version} io.netty - netty-codec + netty-common ${netty.version} - com.h2database - h2-mvstore - 1.4.199 + io.netty + netty-handler + ${netty.version} io.netty - netty-codec-mqtt + netty-handler-proxy ${netty.version} @@ -86,7 +81,7 @@ io.netty - netty-handler + netty-transport ${netty.version} @@ -98,14 +93,14 @@ build-helper-maven-plugin - reserve-network-port + reserve-mqtt-broker-port reserve-network-port process-resources - mqttembeddedbroker.port + mqttbroker.port diff --git a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/Constants.java b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/Constants.java deleted file mode 100644 index f9f5925457..0000000000 --- a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/Constants.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mqtt; - -/** - * MQTT embedded broker constants - * - * @author David Graeff - Initial contribution - */ -public class Constants { - /** - * The broker connection client ID. You can request the embedded broker connection via the MqttService: - * - *
-     * MqttBrokerConnection c = mqttService.getBrokerConnection(Constants.CLIENTID);
-     * 
- */ - public static final String CLIENTID = "embedded-mqtt-broker"; - - /** - * The broker persistent identifier used for identifying configurations. - */ - public static final String PID = "org.openhab.core.mqttembeddedbroker"; - - /** - * The configuration key used for configuring the embedded broker port. - */ - public static final String PORT = "port"; -} diff --git a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/DiscoverComponentsTest.java b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/DiscoverComponentsTest.java deleted file mode 100644 index e94f235810..0000000000 --- a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/DiscoverComponentsTest.java +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mqtt; - -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -import org.openhab.binding.mqtt.generic.AvailabilityTracker; -import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; -import org.openhab.binding.mqtt.generic.TransformationServiceProvider; -import org.openhab.binding.mqtt.homeassistant.internal.ChannelConfigurationTypeAdapterFactory; -import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents; -import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.ComponentDiscovered; -import org.openhab.binding.mqtt.homeassistant.internal.HaID; -import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration; -import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; -import org.openhab.core.test.java.JavaOSGiTest; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -/** - * Tests the {@link DiscoverComponents} class. - * - * @author David Graeff - Initial contribution - */ -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -@NonNullByDefault -public class DiscoverComponentsTest extends JavaOSGiTest { - - private @Mock @NonNullByDefault({}) MqttBrokerConnection connection; - private @Mock @NonNullByDefault({}) ComponentDiscovered discovered; - private @Mock @NonNullByDefault({}) TransformationServiceProvider transformationServiceProvider; - private @Mock @NonNullByDefault({}) ChannelStateUpdateListener channelStateUpdateListener; - private @Mock @NonNullByDefault({}) AvailabilityTracker availabilityTracker; - - @BeforeEach - public void beforeEach() { - CompletableFuture<@Nullable Void> voidFutureComplete = new CompletableFuture<>(); - voidFutureComplete.complete(null); - doReturn(voidFutureComplete).when(connection).unsubscribeAll(); - doReturn(CompletableFuture.completedFuture(true)).when(connection).subscribe(any(), any()); - doReturn(CompletableFuture.completedFuture(true)).when(connection).unsubscribe(any(), any()); - doReturn(CompletableFuture.completedFuture(true)).when(connection).publish(any(), any(), anyInt(), - anyBoolean()); - doReturn(null).when(transformationServiceProvider).getTransformationService(any()); - } - - @Test - public void discoveryTimeTest() throws InterruptedException, ExecutionException, TimeoutException { - // Create a scheduler - ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1); - - Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create(); - - DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.testHomeAssistantThing, - scheduler, channelStateUpdateListener, availabilityTracker, gson, transformationServiceProvider)); - - HandlerConfiguration config = new HandlerConfiguration("homeassistant", - Collections.singletonList("switch/object")); - - Set discoveryIds = new HashSet<>(); - discoveryIds.addAll(HaID.fromConfig(config)); - - discover.startDiscovery(connection, 50, discoveryIds, discovered).get(100, TimeUnit.MILLISECONDS); - } -} diff --git a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/EmbeddedBrokerTools.java b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/EmbeddedBrokerTools.java deleted file mode 100644 index f7e10c4681..0000000000 --- a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/EmbeddedBrokerTools.java +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mqtt; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.util.Dictionary; -import java.util.Hashtable; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; -import org.openhab.core.io.transport.mqtt.MqttConnectionObserver; -import org.openhab.core.io.transport.mqtt.MqttConnectionState; -import org.openhab.core.io.transport.mqtt.MqttService; -import org.openhab.core.io.transport.mqtt.MqttServiceObserver; -import org.osgi.service.cm.Configuration; -import org.osgi.service.cm.ConfigurationAdmin; - -/** - * A full implementation test, that starts the embedded MQTT broker and publishes a homeassistant MQTT discovery device - * tree. - * - * @author David Graeff - Initial contribution - * @author Wouter Born - Support running MQTT itests in parallel by reconfiguring embedded broker port - */ -@NonNullByDefault -public class EmbeddedBrokerTools { - - private static final int BROKER_PORT = Integer.getInteger("mqttembeddedbroker.port", 1883); - - private final ConfigurationAdmin configurationAdmin; - private final MqttService mqttService; - - public @Nullable MqttBrokerConnection embeddedConnection; - - public EmbeddedBrokerTools(ConfigurationAdmin configurationAdmin, MqttService mqttService) { - this.configurationAdmin = configurationAdmin; - this.mqttService = mqttService; - } - - /** - * Request the embedded broker connection from the {@link MqttService} and wait for a connection to be established. - * - * @throws InterruptedException - * @throws IOException - */ - public MqttBrokerConnection waitForConnection() throws InterruptedException, IOException { - reconfigurePort(); - - embeddedConnection = mqttService.getBrokerConnection(Constants.CLIENTID); - if (embeddedConnection == null) { - Semaphore semaphore = new Semaphore(1); - semaphore.acquire(); - MqttServiceObserver observer = new MqttServiceObserver() { - - @Override - public void brokerAdded(String brokerID, MqttBrokerConnection broker) { - if (brokerID.equals(Constants.CLIENTID)) { - embeddedConnection = broker; - semaphore.release(); - } - } - - @Override - public void brokerRemoved(String brokerID, MqttBrokerConnection broker) { - } - }; - mqttService.addBrokersListener(observer); - assertTrue(semaphore.tryAcquire(5, TimeUnit.SECONDS), "Wait for embedded connection client failed"); - } - MqttBrokerConnection embeddedConnection = this.embeddedConnection; - if (embeddedConnection == null) { - throw new IllegalStateException(); - } - - Semaphore semaphore = new Semaphore(1); - semaphore.acquire(); - MqttConnectionObserver mqttConnectionObserver = (state, error) -> { - if (state == MqttConnectionState.CONNECTED) { - semaphore.release(); - } - }; - embeddedConnection.addConnectionObserver(mqttConnectionObserver); - if (embeddedConnection.connectionState() == MqttConnectionState.CONNECTED) { - semaphore.release(); - } - assertTrue(semaphore.tryAcquire(5, TimeUnit.SECONDS), "Connection " + embeddedConnection.getClientId() - + " failed. State: " + embeddedConnection.connectionState()); - return embeddedConnection; - } - - public void reconfigurePort() throws IOException { - Configuration configuration = configurationAdmin.getConfiguration(Constants.PID, null); - - Dictionary properties = configuration.getProperties(); - if (properties == null) { - properties = new Hashtable<>(); - } - - Integer currentPort = (Integer) properties.get(Constants.PORT); - if (currentPort == null || currentPort.intValue() != BROKER_PORT) { - properties.put(Constants.PORT, BROKER_PORT); - configuration.update(properties); - // Remove the connection to make sure the test waits for the new connection to become available - mqttService.removeBrokerConnection(Constants.CLIENTID); - } - } -} diff --git a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/HomeAssistantMQTTImplementationTest.java b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/HomeAssistantMQTTImplementationTest.java deleted file mode 100644 index 0cf38fb2f5..0000000000 --- a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/HomeAssistantMQTTImplementationTest.java +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mqtt; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -import org.openhab.binding.mqtt.generic.AvailabilityTracker; -import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; -import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider; -import org.openhab.binding.mqtt.generic.TransformationServiceProvider; -import org.openhab.binding.mqtt.homeassistant.internal.AbstractComponent; -import org.openhab.binding.mqtt.homeassistant.internal.ChannelConfigurationTypeAdapterFactory; -import org.openhab.binding.mqtt.homeassistant.internal.ComponentSwitch; -import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents; -import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.ComponentDiscovered; -import org.openhab.binding.mqtt.homeassistant.internal.HaID; -import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; -import org.openhab.core.io.transport.mqtt.MqttConnectionObserver; -import org.openhab.core.io.transport.mqtt.MqttConnectionState; -import org.openhab.core.io.transport.mqtt.MqttService; -import org.openhab.core.library.types.OnOffType; -import org.openhab.core.test.java.JavaOSGiTest; -import org.openhab.core.types.State; -import org.openhab.core.types.UnDefType; -import org.openhab.core.util.UIDUtils; -import org.osgi.service.cm.ConfigurationAdmin; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -/** - * A full implementation test, that starts the embedded MQTT broker and publishes a homeassistant MQTT discovery device - * tree. - * - * @author David Graeff - Initial contribution - */ -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -@NonNullByDefault -public class HomeAssistantMQTTImplementationTest extends JavaOSGiTest { - private @NonNullByDefault({}) ConfigurationAdmin configurationAdmin; - private @NonNullByDefault({}) MqttService mqttService; - private @NonNullByDefault({}) MqttBrokerConnection embeddedConnection; - private @NonNullByDefault({}) MqttBrokerConnection connection; - private int registeredTopics = 100; - private @Nullable Throwable failure; - - private @Mock @NonNullByDefault({}) ChannelStateUpdateListener channelStateUpdateListener; - private @Mock @NonNullByDefault({}) AvailabilityTracker availabilityTracker; - private @Mock @NonNullByDefault({}) TransformationServiceProvider transformationServiceProvider; - - /** - * Create an observer that fails the test as soon as the broker client connection changes its connection state - * to something else then CONNECTED. - */ - private final MqttConnectionObserver failIfChange = (state, error) -> assertThat(state, - is(MqttConnectionState.CONNECTED)); - private final String testObjectTopic = "homeassistant/switch/node/" - + ThingChannelConstants.testHomeAssistantThing.getId(); - - @BeforeEach - public void beforeEach() throws Exception { - registerVolatileStorageService(); - configurationAdmin = getService(ConfigurationAdmin.class); - mqttService = getService(MqttService.class); - - // Wait for the EmbeddedBrokerService internal connection to be connected - embeddedConnection = new EmbeddedBrokerTools(configurationAdmin, mqttService).waitForConnection(); - - connection = new MqttBrokerConnection(embeddedConnection.getHost(), embeddedConnection.getPort(), - embeddedConnection.isSecure(), "ha_mqtt"); - connection.start().get(2, TimeUnit.SECONDS); - assertThat(connection.connectionState(), is(MqttConnectionState.CONNECTED)); - - // If the connection state changes in between -> fail - connection.addConnectionObserver(failIfChange); - - // Create topic string and config for one example HA component (a Switch) - final String config = "{'name':'testname','state_topic':'" + testObjectTopic + "/state','command_topic':'" - + testObjectTopic + "/set'}"; - - // Publish component configurations and component states to MQTT - List> futures = new ArrayList<>(); - futures.add(embeddedConnection.publish(testObjectTopic + "/config", config.getBytes(), 0, true)); - futures.add(embeddedConnection.publish(testObjectTopic + "/state", "ON".getBytes(), 0, true)); - - registeredTopics = futures.size(); - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(2, TimeUnit.SECONDS); - - failure = null; - - doReturn(null).when(transformationServiceProvider).getTransformationService(any()); - } - - @AfterEach - public void afterEach() throws Exception { - if (connection != null) { - connection.removeConnectionObserver(failIfChange); - connection.stop().get(2, TimeUnit.SECONDS); - } - } - - @Test - public void reconnectTest() throws InterruptedException, ExecutionException, TimeoutException { - connection.removeConnectionObserver(failIfChange); - connection.stop().get(2, TimeUnit.SECONDS); - connection = new MqttBrokerConnection(embeddedConnection.getHost(), embeddedConnection.getPort(), - embeddedConnection.isSecure(), "ha_mqtt"); - connection.start().get(2, TimeUnit.SECONDS); - } - - @Test - public void retrieveAllTopics() throws InterruptedException, ExecutionException, TimeoutException { - CountDownLatch c = new CountDownLatch(registeredTopics); - connection.subscribe("homeassistant/+/+/" + ThingChannelConstants.testHomeAssistantThing.getId() + "/#", - (topic, payload) -> c.countDown()).get(2, TimeUnit.SECONDS); - assertTrue(c.await(2, TimeUnit.SECONDS), - "Connection " + connection.getClientId() + " not retrieving all topics"); - } - - @Test - public void parseHATree() throws InterruptedException, ExecutionException, TimeoutException { - MqttChannelTypeProvider channelTypeProvider = mock(MqttChannelTypeProvider.class); - - final Map> haComponents = new HashMap<>(); - Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create(); - - ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4); - DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.testHomeAssistantThing, - scheduler, channelStateUpdateListener, availabilityTracker, gson, transformationServiceProvider)); - - // The DiscoverComponents object calls ComponentDiscovered callbacks. - // In the following implementation we add the found component to the `haComponents` map - // and add the types to the channelTypeProvider, like in the real Thing handler. - final CountDownLatch latch = new CountDownLatch(1); - ComponentDiscovered cd = (haID, c) -> { - haComponents.put(c.uid().getId(), c); - c.addChannelTypes(channelTypeProvider); - channelTypeProvider.setChannelGroupType(c.groupTypeUID(), c.type()); - latch.countDown(); - }; - - // Start the discovery for 2000ms. Forced timeout after 4000ms. - HaID haID = new HaID(testObjectTopic + "/config"); - CompletableFuture future = discover.startDiscovery(connection, 2000, Collections.singleton(haID), cd) - .thenRun(() -> { - }).exceptionally(e -> { - failure = e; - return null; - }); - - assertTrue(latch.await(4, TimeUnit.SECONDS)); - future.get(2, TimeUnit.SECONDS); - - // No failure expected and one discovered result - assertNull(failure); - assertThat(haComponents.size(), is(1)); - - // For the switch component we should have one channel group type and one channel type - // setChannelGroupType is called once above - verify(channelTypeProvider, times(2)).setChannelGroupType(any(), any()); - verify(channelTypeProvider, times(1)).setChannelType(any(), any()); - - String channelGroupId = UIDUtils - .encode("node_" + ThingChannelConstants.testHomeAssistantThing.getId() + "_switch"); - - State value = haComponents.get(channelGroupId).channelTypes().get(ComponentSwitch.switchChannelID).getState() - .getCache().getChannelState(); - assertThat(value, is(UnDefType.UNDEF)); - - haComponents.values().stream().map(e -> e.start(connection, scheduler, 100)) - .reduce(CompletableFuture.completedFuture(null), (a, v) -> a.thenCompose(b -> v)).exceptionally(e -> { - failure = e; - return null; - }).get(); - - // We should have received the retained value, while subscribing to the channels MQTT state topic. - verify(channelStateUpdateListener, timeout(4000).times(1)).updateChannelState(any(), any()); - - // Value should be ON now. - value = haComponents.get(channelGroupId).channelTypes().get(ComponentSwitch.switchChannelID).getState() - .getCache().getChannelState(); - assertThat(value, is(OnOffType.ON)); - } -} diff --git a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/ThingChannelConstants.java b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/ThingChannelConstants.java deleted file mode 100644 index 2c9c856cb1..0000000000 --- a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/ThingChannelConstants.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mqtt; - -import static org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants.HOMEASSISTANT_MQTT_THING; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.thing.ThingUID; - -/** - * Static test definitions, like thing, bridge and channel definitions - * - * @author David Graeff - Initial contribution - */ -@NonNullByDefault -public class ThingChannelConstants { - // Common ThingUID and ChannelUIDs - public static final ThingUID testHomeAssistantThing = new ThingUID(HOMEASSISTANT_MQTT_THING, "device234"); -} diff --git a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/DiscoverComponentsTest.java b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/DiscoverComponentsTest.java new file mode 100644 index 0000000000..170f0e439a --- /dev/null +++ b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/DiscoverComponentsTest.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mqtt.homeassistant; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.binding.mqtt.generic.AvailabilityTracker; +import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; +import org.openhab.binding.mqtt.generic.TransformationServiceProvider; +import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents; +import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.ComponentDiscovered; +import org.openhab.binding.mqtt.homeassistant.internal.HaID; +import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration; +import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory; +import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; +import org.openhab.core.test.java.JavaOSGiTest; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Tests the {@link DiscoverComponents} class. + * + * @author David Graeff - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class DiscoverComponentsTest extends JavaOSGiTest { + + private @Mock @NonNullByDefault({}) MqttBrokerConnection connection; + private @Mock @NonNullByDefault({}) ComponentDiscovered discovered; + private @Mock @NonNullByDefault({}) TransformationServiceProvider transformationServiceProvider; + private @Mock @NonNullByDefault({}) ChannelStateUpdateListener channelStateUpdateListener; + private @Mock @NonNullByDefault({}) AvailabilityTracker availabilityTracker; + + @BeforeEach + public void beforeEach() { + CompletableFuture<@Nullable Void> voidFutureComplete = new CompletableFuture<>(); + voidFutureComplete.complete(null); + doReturn(voidFutureComplete).when(connection).unsubscribeAll(); + doReturn(CompletableFuture.completedFuture(true)).when(connection).subscribe(any(), any()); + doReturn(CompletableFuture.completedFuture(true)).when(connection).unsubscribe(any(), any()); + doReturn(CompletableFuture.completedFuture(true)).when(connection).publish(any(), any(), anyInt(), + anyBoolean()); + doReturn(null).when(transformationServiceProvider).getTransformationService(any()); + } + + @Test + public void discoveryTimeTest() throws InterruptedException, ExecutionException, TimeoutException { + // Create a scheduler + ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1); + + Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create(); + + DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING, + scheduler, channelStateUpdateListener, availabilityTracker, gson, transformationServiceProvider)); + + HandlerConfiguration config = new HandlerConfiguration("homeassistant", + Collections.singletonList("switch/object")); + + Set discoveryIds = new HashSet<>(); + discoveryIds.addAll(HaID.fromConfig(config)); + + discover.startDiscovery(connection, 50, discoveryIds, discovered).get(100, TimeUnit.MILLISECONDS); + } +} diff --git a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/HomeAssistantMQTTImplementationTest.java b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/HomeAssistantMQTTImplementationTest.java new file mode 100644 index 0000000000..dde840756c --- /dev/null +++ b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/HomeAssistantMQTTImplementationTest.java @@ -0,0 +1,209 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mqtt.homeassistant; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.binding.mqtt.generic.AvailabilityTracker; +import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; +import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider; +import org.openhab.binding.mqtt.generic.TransformationServiceProvider; +import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents; +import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.ComponentDiscovered; +import org.openhab.binding.mqtt.homeassistant.internal.HaID; +import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent; +import org.openhab.binding.mqtt.homeassistant.internal.component.Switch; +import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory; +import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; +import org.openhab.core.io.transport.mqtt.MqttConnectionObserver; +import org.openhab.core.io.transport.mqtt.MqttConnectionState; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.openhab.core.util.UIDUtils; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * A full implementation test, that starts the embedded MQTT broker and publishes a homeassistant MQTT discovery device + * tree. + * + * @author David Graeff - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest { + + private @NonNullByDefault({}) MqttBrokerConnection haConnection; + private int registeredTopics = 100; + private @Nullable Throwable failure; + + private @Mock @NonNullByDefault({}) ChannelStateUpdateListener channelStateUpdateListener; + private @Mock @NonNullByDefault({}) AvailabilityTracker availabilityTracker; + private @Mock @NonNullByDefault({}) TransformationServiceProvider transformationServiceProvider; + + /** + * Create an observer that fails the test as soon as the broker client connection changes its connection state + * to something else then CONNECTED. + */ + private final MqttConnectionObserver failIfChange = (state, error) -> assertThat(state, + is(MqttConnectionState.CONNECTED)); + private final String testObjectTopic = "homeassistant/switch/node/" + + ThingChannelConstants.TEST_HOME_ASSISTANT_THING.getId(); + + @Override + @BeforeEach + public void beforeEach() throws Exception { + super.beforeEach(); + + haConnection = createBrokerConnection("ha_mqtt"); + + // If the connection state changes in between -> fail + haConnection.addConnectionObserver(failIfChange); + + // Create topic string and config for one example HA component (a Switch) + final String config = "{'name':'testname','state_topic':'" + testObjectTopic + "/state','command_topic':'" + + testObjectTopic + "/set'}"; + + // Publish component configurations and component states to MQTT + List> futures = new ArrayList<>(); + futures.add(publish(testObjectTopic + "/config", config)); + futures.add(publish(testObjectTopic + "/state", "ON")); + + registeredTopics = futures.size(); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(2, TimeUnit.SECONDS); + + failure = null; + + doReturn(null).when(transformationServiceProvider).getTransformationService(any()); + } + + @Override + @AfterEach + public void afterEach() throws Exception { + if (haConnection != null) { + haConnection.removeConnectionObserver(failIfChange); + haConnection.stop().get(5, TimeUnit.SECONDS); + } + + super.afterEach(); + } + + @Test + public void reconnectTest() throws Exception { + haConnection.removeConnectionObserver(failIfChange); + haConnection.stop().get(5, TimeUnit.SECONDS); + haConnection = createBrokerConnection("ha_mqtt"); + } + + @Test + public void retrieveAllTopics() throws Exception { + CountDownLatch c = new CountDownLatch(registeredTopics); + haConnection.subscribe("homeassistant/+/+/" + ThingChannelConstants.TEST_HOME_ASSISTANT_THING.getId() + "/#", + (topic, payload) -> c.countDown()).get(5, TimeUnit.SECONDS); + assertTrue(c.await(2, TimeUnit.SECONDS), + "Connection " + haConnection.getClientId() + " not retrieving all topics"); + } + + @Test + public void parseHATree() throws Exception { + MqttChannelTypeProvider channelTypeProvider = mock(MqttChannelTypeProvider.class); + + final Map> haComponents = new HashMap<>(); + Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create(); + + ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4); + DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING, + scheduler, channelStateUpdateListener, availabilityTracker, gson, transformationServiceProvider)); + + // The DiscoverComponents object calls ComponentDiscovered callbacks. + // In the following implementation we add the found component to the `haComponents` map + // and add the types to the channelTypeProvider, like in the real Thing handler. + final CountDownLatch latch = new CountDownLatch(1); + ComponentDiscovered cd = (haID, c) -> { + haComponents.put(c.getGroupUID().getId(), c); + c.addChannelTypes(channelTypeProvider); + channelTypeProvider.setChannelGroupType(c.getGroupTypeUID(), c.getType()); + latch.countDown(); + }; + + // Start the discovery for 2000ms. Forced timeout after 4000ms. + HaID haID = new HaID(testObjectTopic + "/config"); + CompletableFuture future = discover.startDiscovery(haConnection, 2000, Collections.singleton(haID), cd) + .thenRun(() -> { + }).exceptionally(e -> { + failure = e; + return null; + }); + + assertTrue(latch.await(4, TimeUnit.SECONDS)); + future.get(5, TimeUnit.SECONDS); + + // No failure expected and one discovered result + assertNull(failure); + assertThat(haComponents.size(), is(1)); + + // For the switch component we should have one channel group type and one channel type + // setChannelGroupType is called once above + verify(channelTypeProvider, times(2)).setChannelGroupType(any(), any()); + verify(channelTypeProvider, times(1)).setChannelType(any(), any()); + + String channelGroupId = UIDUtils + .encode("node_" + ThingChannelConstants.TEST_HOME_ASSISTANT_THING.getId() + "_switch"); + + State value = haComponents.get(channelGroupId).getChannel(Switch.SWITCH_CHANNEL_ID).getState().getCache() + .getChannelState(); + assertThat(value, is(UnDefType.UNDEF)); + + haComponents.values().stream().map(e -> e.start(haConnection, scheduler, 100)) + .reduce(CompletableFuture.completedFuture(null), (a, v) -> a.thenCompose(b -> v)).exceptionally(e -> { + failure = e; + return null; + }).get(); + + // We should have received the retained value, while subscribing to the channels MQTT state topic. + verify(channelStateUpdateListener, timeout(4000).times(1)).updateChannelState(any(), any()); + + // Value should be ON now. + value = haComponents.get(channelGroupId).getChannel(Switch.SWITCH_CHANNEL_ID).getState().getCache() + .getChannelState(); + assertThat(value, is(OnOffType.ON)); + } +} diff --git a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/MqttOSGiTest.java b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/MqttOSGiTest.java new file mode 100644 index 0000000000..79415f70aa --- /dev/null +++ b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/MqttOSGiTest.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mqtt.homeassistant; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.nio.charset.StandardCharsets; +import java.util.Properties; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; +import org.openhab.core.io.transport.mqtt.MqttConnectionState; +import org.openhab.core.test.java.JavaOSGiTest; + +import io.moquette.BrokerConstants; +import io.moquette.broker.Server; + +/** + * Creates a Moquette MQTT broker instance and a {@link MqttBrokerConnection} for testing MQTT bindings. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class MqttOSGiTest extends JavaOSGiTest { + + private static final String BROKER_ID = "test-broker"; + private static final int BROKER_PORT = Integer.getInteger("mqttbroker.port", 1883); + + protected @NonNullByDefault({}) MqttBrokerConnection brokerConnection; + + private Server moquetteServer = new Server(); + + @BeforeEach + public void beforeEach() throws Exception { + registerVolatileStorageService(); + + moquetteServer = new Server(); + moquetteServer.startServer(brokerProperties()); + + brokerConnection = createBrokerConnection(BROKER_ID); + } + + @AfterEach + public void afterEach() throws Exception { + brokerConnection.stop().get(5, TimeUnit.SECONDS); + moquetteServer.stopServer(); + } + + private Properties brokerProperties() { + Properties properties = new Properties(); + properties.put(BrokerConstants.HOST_PROPERTY_NAME, BrokerConstants.HOST); + properties.put(BrokerConstants.PORT_PROPERTY_NAME, String.valueOf(BROKER_PORT)); + properties.put(BrokerConstants.SSL_PORT_PROPERTY_NAME, BrokerConstants.DISABLED_PORT_BIND); + properties.put(BrokerConstants.WEB_SOCKET_PORT_PROPERTY_NAME, BrokerConstants.DISABLED_PORT_BIND); + properties.put(BrokerConstants.WSS_PORT_PROPERTY_NAME, BrokerConstants.DISABLED_PORT_BIND); + return properties; + } + + protected MqttBrokerConnection createBrokerConnection(String clientId) throws Exception { + MqttBrokerConnection connection = new MqttBrokerConnection(BrokerConstants.HOST, BROKER_PORT, false, clientId); + connection.setQos(1); + connection.start().get(5, TimeUnit.SECONDS); + + waitForAssert(() -> assertThat(connection.connectionState(), is(MqttConnectionState.CONNECTED))); + + return connection; + } + + protected CompletableFuture publish(String topic, String message) { + return brokerConnection.publish(topic, message.getBytes(StandardCharsets.UTF_8), 1, true); + } +} diff --git a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/ThingChannelConstants.java b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/ThingChannelConstants.java new file mode 100644 index 0000000000..5d69d00ee4 --- /dev/null +++ b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/ThingChannelConstants.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mqtt.homeassistant; + +import static org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants.HOMEASSISTANT_MQTT_THING; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingUID; + +/** + * Static test definitions, like thing, bridge and channel definitions + * + * @author David Graeff - Initial contribution + */ +@NonNullByDefault +public class ThingChannelConstants { + // Common ThingUID and ChannelUIDs + public static final ThingUID TEST_HOME_ASSISTANT_THING = new ThingUID(HOMEASSISTANT_MQTT_THING, "device234"); +} diff --git a/itests/org.openhab.binding.mqtt.homie.tests/itest.bndrun b/itests/org.openhab.binding.mqtt.homie.tests/itest.bndrun index 2c862325e9..304906cf25 100644 --- a/itests/org.openhab.binding.mqtt.homie.tests/itest.bndrun +++ b/itests/org.openhab.binding.mqtt.homie.tests/itest.bndrun @@ -3,93 +3,111 @@ Bundle-SymbolicName: ${project.artifactId} Fragment-Host: org.openhab.binding.mqtt.homie +Import-Package: \ + com.bugsnag.*;resolution:=optional,\ + com.librato.metrics.reporter.*;resolution:=optional,\ + * + +-includeresource: \ + moquette-broker-[0-9.]*.jar;lib:=true + -runrequires: \ bnd.identity;id='org.openhab.binding.mqtt.homie.tests',\ bnd.identity;id='org.openhab.core.binding.xml',\ - bnd.identity;id='org.openhab.core.thing.xml',\ - bnd.identity;id='org.openhab.io.mqttembeddedbroker' + bnd.identity;id='org.openhab.core.thing.xml' # We would like to use the "volatile" storage only -runblacklist: \ bnd.identity;id='org.openhab.core.storage.json' --runvm: \ +-runvm.mqtt: \ -Dio.netty.noUnsafe=true,\ - -Dmqttembeddedbroker.port=${mqttembeddedbroker.port} + -Dmqttbroker.port=${mqttbroker.port} # # done # -runbundles: \ - ch.qos.logback.core;version='[1.2.3,1.2.4)',\ - com.google.gson;version='[2.8.2,2.8.3)',\ - javax.measure.unit-api;version='[1.0.0,1.0.1)',\ - org.apache.commons.lang;version='[2.6.0,2.6.1)',\ - org.apache.felix.configadmin;version='[1.9.8,1.9.9)',\ org.apache.felix.http.servlet-api;version='[1.1.2,1.1.3)',\ - org.apache.felix.scr;version='[2.1.10,2.1.11)',\ org.eclipse.equinox.event;version='[1.4.300,1.4.301)',\ - org.objenesis;version='[2.6.0,2.6.1)',\ org.osgi.service.event;version='[1.4.0,1.4.1)',\ - slf4j.api;version='[1.7.25,1.7.26)',\ - org.apache.servicemix.bundles.xstream;version='[1.4.7,1.4.8)',\ - com.h2database.mvstore;version='[1.4.199,1.4.200)',\ - io.netty.buffer;version='[4.1.42,4.1.43)',\ - io.netty.codec;version='[4.1.42,4.1.43)',\ - io.netty.codec-mqtt;version='[4.1.42,4.1.43)',\ - io.netty.common;version='[4.1.42,4.1.43)',\ - io.netty.handler;version='[4.1.42,4.1.43)',\ - io.netty.resolver;version='[4.1.42,4.1.43)',\ - io.netty.transport;version='[4.1.42,4.1.43)',\ - tec.uom.lib.uom-lib-common;version='[1.0.3,1.0.4)',\ - tec.uom.se;version='[1.0.10,1.0.11)',\ - ch.qos.logback.classic;version='[1.2.3,1.2.4)',\ - biz.aQute.tester.junit-platform;version='[5.1.2,5.1.3)',\ - com.google.dagger;version='[2.20.0,2.20.1)',\ - com.hivemq.client.mqtt;version='[1.1.2,1.1.3)',\ - io.netty.codec-http;version='[4.1.34,4.1.35)',\ - io.netty.transport-native-epoll;version='[4.1.34,4.1.35)',\ - io.netty.transport-native-unix-common;version='[4.1.34,4.1.35)',\ - io.reactivex.rxjava2.rxjava;version='[2.2.5,2.2.6)',\ - junit-jupiter-api;version='[5.6.2,5.6.3)',\ - junit-jupiter-engine;version='[5.6.2,5.6.3)',\ - junit-platform-commons;version='[1.6.2,1.6.3)',\ - junit-platform-engine;version='[1.6.2,1.6.3)',\ - junit-platform-launcher;version='[1.6.2,1.6.3)',\ - net.bytebuddy.byte-buddy;version='[1.10.13,1.10.14)',\ - net.bytebuddy.byte-buddy-agent;version='[1.10.13,1.10.14)',\ - org.apache.aries.javax.jax.rs-api;version='[1.0.0,1.0.1)',\ - org.apache.commons.codec;version='[1.10.0,1.10.1)',\ - org.eclipse.jetty.http;version='[9.4.20,9.4.21)',\ - org.eclipse.jetty.io;version='[9.4.20,9.4.21)',\ - org.eclipse.jetty.security;version='[9.4.20,9.4.21)',\ - org.eclipse.jetty.server;version='[9.4.20,9.4.21)',\ - org.eclipse.jetty.servlet;version='[9.4.20,9.4.21)',\ - org.eclipse.jetty.util;version='[9.4.20,9.4.21)',\ org.glassfish.hk2.external.javax.inject;version='[2.4.0,2.4.1)',\ org.hamcrest;version='[2.2.0,2.2.1)',\ org.jctools.core;version='[2.1.2,2.1.3)',\ - org.mockito.mockito-core;version='[3.4.6,3.4.7)',\ - org.openhab.binding.mqtt;version='[3.0.0,3.0.1)',\ - org.openhab.binding.mqtt.generic;version='[3.0.0,3.0.1)',\ - org.openhab.binding.mqtt.homie;version='[3.0.0,3.0.1)',\ - org.openhab.binding.mqtt.homie.tests;version='[3.0.0,3.0.1)',\ - org.openhab.core;version='[3.0.0,3.0.1)',\ - org.openhab.core.binding.xml;version='[3.0.0,3.0.1)',\ - org.openhab.core.config.core;version='[3.0.0,3.0.1)',\ - org.openhab.core.config.discovery;version='[3.0.0,3.0.1)',\ - org.openhab.core.config.xml;version='[3.0.0,3.0.1)',\ - org.openhab.core.io.console;version='[3.0.0,3.0.1)',\ - org.openhab.core.io.transport.mqtt;version='[3.0.0,3.0.1)',\ - org.openhab.core.test;version='[3.0.0,3.0.1)',\ - org.openhab.core.thing;version='[3.0.0,3.0.1)',\ - org.openhab.core.thing.xml;version='[3.0.0,3.0.1)',\ - org.openhab.core.transform;version='[3.0.0,3.0.1)',\ - org.openhab.io.mqttembeddedbroker;version='[3.0.0,3.0.1)',\ org.opentest4j;version='[1.2.0,1.2.1)',\ - org.reactivestreams.reactive-streams;version='[1.0.2,1.0.3)',\ jakarta.xml.bind-api;version='[2.3.3,2.3.4)',\ com.sun.xml.bind.jaxb-osgi;version='[2.3.3,2.3.4)',\ - org.glassfish.hk2.osgi-resource-locator;version='[1.0.1,1.0.2)',\ - org.apache.servicemix.specs.activation-api-1.2.1;version='[1.2.1,1.2.2)' + org.apache.servicemix.specs.activation-api-1.2.1;version='[1.2.1,1.2.2)',\ + com.google.dagger;version='[2.27.0,2.27.1)',\ + com.google.gson;version='[2.8.9,2.8.10)',\ + com.hivemq.client.mqtt;version='[1.2.2,1.2.3)',\ + io.netty.buffer;version='[4.1.72,4.1.73)',\ + io.netty.codec;version='[4.1.72,4.1.73)',\ + io.netty.codec-http;version='[4.1.59,4.1.60)',\ + io.netty.codec-socks;version='[4.1.72,4.1.73)',\ + io.netty.common;version='[4.1.72,4.1.73)',\ + io.netty.handler;version='[4.1.72,4.1.73)',\ + io.netty.handler-proxy;version='[4.1.72,4.1.73)',\ + io.netty.resolver;version='[4.1.72,4.1.73)',\ + io.netty.tcnative-classes;version='[2.0.46,2.0.47)',\ + io.netty.transport;version='[4.1.72,4.1.73)',\ + io.netty.transport-native-epoll;version='[4.1.59,4.1.60)',\ + io.netty.transport-native-unix-common;version='[4.1.59,4.1.60)',\ + io.reactivex.rxjava2.rxjava;version='[2.2.19,2.2.20)',\ + jakarta.annotation-api;version='[2.0.0,2.0.1)',\ + jakarta.inject.jakarta.inject-api;version='[2.0.0,2.0.1)',\ + javax.measure.unit-api;version='[2.1.2,2.1.3)',\ + junit-jupiter-api;version='[5.8.1,5.8.2)',\ + junit-jupiter-engine;version='[5.8.1,5.8.2)',\ + junit-platform-commons;version='[1.8.1,1.8.2)',\ + junit-platform-engine;version='[1.8.1,1.8.2)',\ + junit-platform-launcher;version='[1.8.1,1.8.2)',\ + net.bytebuddy.byte-buddy;version='[1.12.1,1.12.2)',\ + net.bytebuddy.byte-buddy-agent;version='[1.12.1,1.12.2)',\ + org.apache.aries.javax.jax.rs-api;version='[1.0.1,1.0.2)',\ + org.apache.felix.configadmin;version='[1.9.22,1.9.23)',\ + org.apache.felix.scr;version='[2.1.30,2.1.31)',\ + org.eclipse.jetty.http;version='[9.4.43,9.4.44)',\ + org.eclipse.jetty.io;version='[9.4.43,9.4.44)',\ + org.eclipse.jetty.security;version='[9.4.43,9.4.44)',\ + org.eclipse.jetty.server;version='[9.4.43,9.4.44)',\ + org.eclipse.jetty.servlet;version='[9.4.43,9.4.44)',\ + org.eclipse.jetty.util;version='[9.4.43,9.4.44)',\ + org.eclipse.jetty.util.ajax;version='[9.4.43,9.4.44)',\ + org.glassfish.hk2.osgi-resource-locator;version='[1.0.3,1.0.4)',\ + org.jsr-305;version='[3.0.2,3.0.3)',\ + org.mockito.junit-jupiter;version='[4.1.0,4.1.1)',\ + org.mockito.mockito-core;version='[4.1.0,4.1.1)',\ + org.objenesis;version='[3.2.0,3.2.1)',\ + org.openhab.binding.mqtt;version='[3.3.0,3.3.1)',\ + org.openhab.binding.mqtt.generic;version='[3.3.0,3.3.1)',\ + org.openhab.binding.mqtt.homie;version='[3.3.0,3.3.1)',\ + org.openhab.binding.mqtt.homie.tests;version='[3.3.0,3.3.1)',\ + org.openhab.core;version='[3.3.0,3.3.1)',\ + org.openhab.core.binding.xml;version='[3.3.0,3.3.1)',\ + org.openhab.core.config.core;version='[3.3.0,3.3.1)',\ + org.openhab.core.config.discovery;version='[3.3.0,3.3.1)',\ + org.openhab.core.config.xml;version='[3.3.0,3.3.1)',\ + org.openhab.core.io.console;version='[3.3.0,3.3.1)',\ + org.openhab.core.io.transport.mqtt;version='[3.3.0,3.3.1)',\ + org.openhab.core.test;version='[3.3.0,3.3.1)',\ + org.openhab.core.thing;version='[3.3.0,3.3.1)',\ + org.openhab.core.thing.xml;version='[3.3.0,3.3.1)',\ + org.openhab.core.transform;version='[3.3.0,3.3.1)',\ + org.ops4j.pax.logging.pax-logging-api;version='[2.0.14,2.0.15)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)',\ + org.osgi.util.function;version='[1.2.0,1.2.1)',\ + org.osgi.util.promise;version='[1.2.0,1.2.1)',\ + org.reactivestreams.reactive-streams;version='[1.0.3,1.0.4)',\ + si-units;version='[2.1.0,2.1.1)',\ + si.uom.si-quantity;version='[2.1.0,2.1.1)',\ + tech.units.indriya;version='[2.1.2,2.1.3)',\ + uom-lib-common;version='[2.1.0,2.1.1)',\ + xstream;version='[1.4.19,1.4.20)',\ + com.h2database.mvstore;version='[1.4.199,1.4.200)',\ + com.zaxxer.HikariCP;version='[2.4.7,2.4.8)',\ + io.dropwizard.metrics.core;version='[3.2.2,3.2.3)',\ + io.netty.codec-mqtt;version='[4.1.72,4.1.73)',\ + org.apache.commons.codec;version='[1.10.0,1.10.1)',\ + biz.aQute.tester.junit-platform;version='[6.2.0,6.2.1)' diff --git a/itests/org.openhab.binding.mqtt.homie.tests/pom.xml b/itests/org.openhab.binding.mqtt.homie.tests/pom.xml index a77c9bc766..a6aa0a365c 100644 --- a/itests/org.openhab.binding.mqtt.homie.tests/pom.xml +++ b/itests/org.openhab.binding.mqtt.homie.tests/pom.xml @@ -7,13 +7,17 @@ org.openhab.addons.itests org.openhab.addons.reactor.itests - 3.1.0-SNAPSHOT + 3.3.0-SNAPSHOT org.openhab.binding.mqtt.homie.tests openHAB Add-ons :: Integration Tests :: MQTT Homie Tests + + 1883 + + org.openhab.addons.bundles @@ -31,52 +35,43 @@ ${project.version} - com.github.j-n-k + com.h2database + h2-mvstore + 1.4.199 + + + io.moquette moquette-broker - 0.13.0.OH2 - - - org.slf4j - slf4j-api - - - org.slf4j - slf4j-log4j12 - - - org.mockito - mockito-core - - + 0.15 io.netty - netty-common + netty-buffer ${netty.version} io.netty - netty-buffer + netty-codec ${netty.version} io.netty - netty-transport + netty-codec-mqtt ${netty.version} io.netty - netty-codec + netty-common ${netty.version} - com.h2database - h2-mvstore - 1.4.199 + io.netty + netty-handler + ${netty.version} io.netty - netty-codec-mqtt + netty-handler-proxy ${netty.version} @@ -86,7 +81,7 @@ io.netty - netty-handler + netty-transport ${netty.version} @@ -98,14 +93,14 @@ build-helper-maven-plugin - reserve-network-port + reserve-mqtt-broker-port reserve-network-port process-resources - mqttembeddedbroker.port + mqttbroker.port diff --git a/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/Constants.java b/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/Constants.java deleted file mode 100644 index f9f5925457..0000000000 --- a/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/Constants.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mqtt; - -/** - * MQTT embedded broker constants - * - * @author David Graeff - Initial contribution - */ -public class Constants { - /** - * The broker connection client ID. You can request the embedded broker connection via the MqttService: - * - *
-     * MqttBrokerConnection c = mqttService.getBrokerConnection(Constants.CLIENTID);
-     * 
- */ - public static final String CLIENTID = "embedded-mqtt-broker"; - - /** - * The broker persistent identifier used for identifying configurations. - */ - public static final String PID = "org.openhab.core.mqttembeddedbroker"; - - /** - * The configuration key used for configuring the embedded broker port. - */ - public static final String PORT = "port"; -} diff --git a/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/EmbeddedBrokerTools.java b/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/EmbeddedBrokerTools.java deleted file mode 100644 index f7e10c4681..0000000000 --- a/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/EmbeddedBrokerTools.java +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mqtt; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.util.Dictionary; -import java.util.Hashtable; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; -import org.openhab.core.io.transport.mqtt.MqttConnectionObserver; -import org.openhab.core.io.transport.mqtt.MqttConnectionState; -import org.openhab.core.io.transport.mqtt.MqttService; -import org.openhab.core.io.transport.mqtt.MqttServiceObserver; -import org.osgi.service.cm.Configuration; -import org.osgi.service.cm.ConfigurationAdmin; - -/** - * A full implementation test, that starts the embedded MQTT broker and publishes a homeassistant MQTT discovery device - * tree. - * - * @author David Graeff - Initial contribution - * @author Wouter Born - Support running MQTT itests in parallel by reconfiguring embedded broker port - */ -@NonNullByDefault -public class EmbeddedBrokerTools { - - private static final int BROKER_PORT = Integer.getInteger("mqttembeddedbroker.port", 1883); - - private final ConfigurationAdmin configurationAdmin; - private final MqttService mqttService; - - public @Nullable MqttBrokerConnection embeddedConnection; - - public EmbeddedBrokerTools(ConfigurationAdmin configurationAdmin, MqttService mqttService) { - this.configurationAdmin = configurationAdmin; - this.mqttService = mqttService; - } - - /** - * Request the embedded broker connection from the {@link MqttService} and wait for a connection to be established. - * - * @throws InterruptedException - * @throws IOException - */ - public MqttBrokerConnection waitForConnection() throws InterruptedException, IOException { - reconfigurePort(); - - embeddedConnection = mqttService.getBrokerConnection(Constants.CLIENTID); - if (embeddedConnection == null) { - Semaphore semaphore = new Semaphore(1); - semaphore.acquire(); - MqttServiceObserver observer = new MqttServiceObserver() { - - @Override - public void brokerAdded(String brokerID, MqttBrokerConnection broker) { - if (brokerID.equals(Constants.CLIENTID)) { - embeddedConnection = broker; - semaphore.release(); - } - } - - @Override - public void brokerRemoved(String brokerID, MqttBrokerConnection broker) { - } - }; - mqttService.addBrokersListener(observer); - assertTrue(semaphore.tryAcquire(5, TimeUnit.SECONDS), "Wait for embedded connection client failed"); - } - MqttBrokerConnection embeddedConnection = this.embeddedConnection; - if (embeddedConnection == null) { - throw new IllegalStateException(); - } - - Semaphore semaphore = new Semaphore(1); - semaphore.acquire(); - MqttConnectionObserver mqttConnectionObserver = (state, error) -> { - if (state == MqttConnectionState.CONNECTED) { - semaphore.release(); - } - }; - embeddedConnection.addConnectionObserver(mqttConnectionObserver); - if (embeddedConnection.connectionState() == MqttConnectionState.CONNECTED) { - semaphore.release(); - } - assertTrue(semaphore.tryAcquire(5, TimeUnit.SECONDS), "Connection " + embeddedConnection.getClientId() - + " failed. State: " + embeddedConnection.connectionState()); - return embeddedConnection; - } - - public void reconfigurePort() throws IOException { - Configuration configuration = configurationAdmin.getConfiguration(Constants.PID, null); - - Dictionary properties = configuration.getProperties(); - if (properties == null) { - properties = new Hashtable<>(); - } - - Integer currentPort = (Integer) properties.get(Constants.PORT); - if (currentPort == null || currentPort.intValue() != BROKER_PORT) { - properties.put(Constants.PORT, BROKER_PORT); - configuration.update(properties); - // Remove the connection to make sure the test waits for the new connection to become available - mqttService.removeBrokerConnection(Constants.CLIENTID); - } - } -} diff --git a/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/HomieImplementationTest.java b/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/HomieImplementationTest.java deleted file mode 100644 index be6873179b..0000000000 --- a/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/HomieImplementationTest.java +++ /dev/null @@ -1,328 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mqtt; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -import org.openhab.binding.mqtt.generic.ChannelState; -import org.openhab.binding.mqtt.generic.tools.ChildMap; -import org.openhab.binding.mqtt.generic.tools.WaitForTopicValue; -import org.openhab.binding.mqtt.homie.internal.handler.HomieThingHandler; -import org.openhab.binding.mqtt.homie.internal.homie300.Device; -import org.openhab.binding.mqtt.homie.internal.homie300.DeviceAttributes; -import org.openhab.binding.mqtt.homie.internal.homie300.DeviceAttributes.ReadyState; -import org.openhab.binding.mqtt.homie.internal.homie300.DeviceCallback; -import org.openhab.binding.mqtt.homie.internal.homie300.Node; -import org.openhab.binding.mqtt.homie.internal.homie300.NodeAttributes; -import org.openhab.binding.mqtt.homie.internal.homie300.Property; -import org.openhab.binding.mqtt.homie.internal.homie300.PropertyAttributes; -import org.openhab.binding.mqtt.homie.internal.homie300.PropertyAttributes.DataTypeEnum; -import org.openhab.binding.mqtt.homie.internal.homie300.PropertyHelper; -import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; -import org.openhab.core.io.transport.mqtt.MqttConnectionObserver; -import org.openhab.core.io.transport.mqtt.MqttConnectionState; -import org.openhab.core.io.transport.mqtt.MqttService; -import org.openhab.core.library.types.DecimalType; -import org.openhab.core.library.types.OnOffType; -import org.openhab.core.test.java.JavaOSGiTest; -import org.openhab.core.types.UnDefType; -import org.osgi.service.cm.ConfigurationAdmin; - -/** - * A full implementation test, that starts the embedded MQTT broker and publishes a homie device tree. - * - * @author David Graeff - Initial contribution - */ -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -@NonNullByDefault -public class HomieImplementationTest extends JavaOSGiTest { - private static final String BASE_TOPIC = "homie"; - private static final String DEVICE_ID = ThingChannelConstants.testHomieThing.getId(); - private static final String DEVICE_TOPIC = BASE_TOPIC + "/" + DEVICE_ID; - - private @NonNullByDefault({}) ConfigurationAdmin configurationAdmin; - private @NonNullByDefault({}) MqttService mqttService; - private @NonNullByDefault({}) MqttBrokerConnection embeddedConnection; - private @NonNullByDefault({}) MqttBrokerConnection connection; - private int registeredTopics = 100; - - // The handler is not tested here, so just mock the callback - private @Mock @NonNullByDefault({}) DeviceCallback callback; - - // A handler mock is required to verify that channel value changes have been received - private @Mock @NonNullByDefault({}) HomieThingHandler handler; - - private @NonNullByDefault({}) ScheduledExecutorService scheduler; - - /** - * Create an observer that fails the test as soon as the broker client connection changes its connection state - * to something else then CONNECTED. - */ - private MqttConnectionObserver failIfChange = (state, error) -> assertThat(state, - is(MqttConnectionState.CONNECTED)); - - private String propertyTestTopic = ""; - - @BeforeEach - public void beforeEach() throws Exception { - registerVolatileStorageService(); - configurationAdmin = getService(ConfigurationAdmin.class); - mqttService = getService(MqttService.class); - - // Wait for the EmbeddedBrokerService internal connection to be connected - embeddedConnection = new EmbeddedBrokerTools(configurationAdmin, mqttService).waitForConnection(); - embeddedConnection.setQos(1); - - connection = new MqttBrokerConnection(embeddedConnection.getHost(), embeddedConnection.getPort(), - embeddedConnection.isSecure(), "homie"); - connection.setQos(1); - connection.start().get(5, TimeUnit.SECONDS); - assertThat(connection.connectionState(), is(MqttConnectionState.CONNECTED)); - // If the connection state changes in between -> fail - connection.addConnectionObserver(failIfChange); - - List> futures = new ArrayList<>(); - futures.add(publish(DEVICE_TOPIC + "/$homie", "3.0")); - futures.add(publish(DEVICE_TOPIC + "/$name", "Name")); - futures.add(publish(DEVICE_TOPIC + "/$state", "ready")); - futures.add(publish(DEVICE_TOPIC + "/$nodes", "testnode")); - - // Add homie node topics - final String testNode = DEVICE_TOPIC + "/testnode"; - futures.add(publish(testNode + "/$name", "Testnode")); - futures.add(publish(testNode + "/$type", "Type")); - futures.add(publish(testNode + "/$properties", "temperature,doorbell,testRetain")); - - // Add homie property topics - final String property = testNode + "/temperature"; - futures.add(publish(property, "10")); - futures.add(publish(property + "/$name", "Testprop")); - futures.add(publish(property + "/$settable", "true")); - futures.add(publish(property + "/$unit", "°C")); - futures.add(publish(property + "/$datatype", "float")); - futures.add(publish(property + "/$format", "-100:100")); - - final String propertyBellTopic = testNode + "/doorbell"; - futures.add(publish(propertyBellTopic + "/$name", "Doorbell")); - futures.add(publish(propertyBellTopic + "/$settable", "false")); - futures.add(publish(propertyBellTopic + "/$retained", "false")); - futures.add(publish(propertyBellTopic + "/$datatype", "boolean")); - - this.propertyTestTopic = testNode + "/testRetain"; - futures.add(publish(propertyTestTopic + "/$name", "Test")); - futures.add(publish(propertyTestTopic + "/$settable", "true")); - futures.add(publish(propertyTestTopic + "/$retained", "false")); - futures.add(publish(propertyTestTopic + "/$datatype", "boolean")); - - registeredTopics = futures.size(); - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(2, TimeUnit.SECONDS); - - scheduler = new ScheduledThreadPoolExecutor(6); - } - - private CompletableFuture publish(String topic, String message) { - return embeddedConnection.publish(topic, message.getBytes(StandardCharsets.UTF_8), 0, true); - } - - @AfterEach - public void afterEach() throws Exception { - if (connection != null) { - connection.removeConnectionObserver(failIfChange); - connection.stop().get(2, TimeUnit.SECONDS); - } - if (scheduler != null) { - scheduler.shutdownNow(); - } - } - - @Test - public void retrieveAllTopics() throws InterruptedException, ExecutionException, TimeoutException { - // four topics are not under /testnode ! - CountDownLatch c = new CountDownLatch(registeredTopics - 4); - connection.subscribe(DEVICE_TOPIC + "/testnode/#", (topic, payload) -> c.countDown()).get(5, TimeUnit.SECONDS); - assertTrue(c.await(5, TimeUnit.SECONDS), - "Connection " + connection.getClientId() + " not retrieving all topics "); - } - - @Test - public void retrieveOneAttribute() throws InterruptedException, ExecutionException { - WaitForTopicValue watcher = new WaitForTopicValue(connection, DEVICE_TOPIC + "/$homie"); - assertThat(watcher.waitForTopicValue(1000), is("3.0")); - } - - @SuppressWarnings("null") - @Test - public void retrieveAttributes() throws InterruptedException, ExecutionException { - assertThat(connection.hasSubscribers(), is(false)); - - Node node = new Node(DEVICE_TOPIC, "testnode", ThingChannelConstants.testHomieThing, callback, - new NodeAttributes()); - Property property = spy( - new Property(DEVICE_TOPIC + "/testnode", node, "temperature", callback, new PropertyAttributes())); - - // Create a scheduler - ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4); - - property.subscribe(connection, scheduler, 500).get(); - - assertThat(property.attributes.settable, is(true)); - assertThat(property.attributes.retained, is(true)); - assertThat(property.attributes.name, is("Testprop")); - assertThat(property.attributes.unit, is("°C")); - assertThat(property.attributes.datatype, is(DataTypeEnum.float_)); - waitForAssert(() -> assertThat(property.attributes.format, is("-100:100"))); - verify(property, timeout(500).atLeastOnce()).attributesReceived(); - - // Receive property value - ChannelState channelState = spy(property.getChannelState()); - PropertyHelper.setChannelState(property, channelState); - - property.startChannel(connection, scheduler, 500).get(); - verify(channelState).start(any(), any(), anyInt()); - verify(channelState, timeout(500)).processMessage(any(), any()); - verify(callback).updateChannelState(any(), any()); - - assertThat(property.getChannelState().getCache().getChannelState(), is(new DecimalType(10))); - - property.stop().get(); - assertThat(connection.hasSubscribers(), is(false)); - } - - // Inject a spy'ed property - public Property createSpyProperty(InvocationOnMock invocation) { - final Node node = (Node) invocation.getMock(); - final String id = (String) invocation.getArguments()[0]; - return spy(node.createProperty(id, spy(new PropertyAttributes()))); - } - - // Inject a spy'ed node - public Node createSpyNode(InvocationOnMock invocation) { - final Device device = (Device) invocation.getMock(); - final String id = (String) invocation.getArguments()[0]; - // Create the node - Node node = spy(device.createNode(id, spy(new NodeAttributes()))); - // Intercept creating a property in the next call and inject a spy'ed property. - doAnswer(this::createSpyProperty).when(node).createProperty(any()); - return node; - } - - @SuppressWarnings("null") - @Test - public void parseHomieTree() throws InterruptedException, ExecutionException, TimeoutException { - // Create a Homie Device object. Because spied Nodes are required for call verification, - // the full Device constructor need to be used and a ChildMap object need to be created manually. - ChildMap nodeMap = new ChildMap<>(); - Device device = spy( - new Device(ThingChannelConstants.testHomieThing, callback, new DeviceAttributes(), nodeMap)); - - // Intercept creating a node in initialize()->start() and inject a spy'ed node. - doAnswer(this::createSpyNode).when(device).createNode(any()); - - // initialize the device, subscribe and wait. - device.initialize(BASE_TOPIC, DEVICE_ID, Collections.emptyList()); - device.subscribe(connection, scheduler, 1500).get(); - - assertThat(device.isInitialized(), is(true)); - - // Check device attributes - assertThat(device.attributes.homie, is("3.0")); - assertThat(device.attributes.name, is("Name")); - assertThat(device.attributes.state, is(ReadyState.ready)); - assertThat(device.attributes.nodes.length, is(1)); - verify(device, times(4)).attributeChanged(any(), any(), any(), any(), anyBoolean()); - verify(callback).readyStateChanged(eq(ReadyState.ready)); - verify(device).attributesReceived(any(), any(), anyInt()); - - // Expect 1 node - assertThat(device.nodes.size(), is(1)); - - // Check node and node attributes - Node node = device.nodes.get("testnode"); - verify(node).subscribe(any(), any(), anyInt()); - verify(node).attributesReceived(any(), any(), anyInt()); - verify(node.attributes).subscribeAndReceive(any(), any(), anyString(), any(), anyInt()); - assertThat(node.attributes.type, is("Type")); - assertThat(node.attributes.name, is("Testnode")); - - // Expect 2 property - assertThat(node.properties.size(), is(3)); - - // Check property and property attributes - Property property = node.properties.get("temperature"); - assertThat(property.attributes.settable, is(true)); - assertThat(property.attributes.retained, is(true)); - assertThat(property.attributes.name, is("Testprop")); - assertThat(property.attributes.unit, is("°C")); - assertThat(property.attributes.datatype, is(DataTypeEnum.float_)); - assertThat(property.attributes.format, is("-100:100")); - verify(property).attributesReceived(); - assertNotNull(property.getChannelState()); - assertThat(property.getType().getState().getMinimum().intValue(), is(-100)); - assertThat(property.getType().getState().getMaximum().intValue(), is(100)); - - // Check property and property attributes - Property propertyBell = node.properties.get("doorbell"); - verify(propertyBell).attributesReceived(); - assertThat(propertyBell.attributes.settable, is(false)); - assertThat(propertyBell.attributes.retained, is(false)); - assertThat(propertyBell.attributes.name, is("Doorbell")); - assertThat(propertyBell.attributes.datatype, is(DataTypeEnum.boolean_)); - - // The device->node->property tree is ready. Now subscribe to property values. - device.startChannels(connection, scheduler, 50, handler).get(); - assertThat(propertyBell.getChannelState().isStateful(), is(false)); - assertThat(propertyBell.getChannelState().getCache().getChannelState(), is(UnDefType.UNDEF)); - assertThat(property.getChannelState().getCache().getChannelState(), is(new DecimalType(10))); - - property = node.properties.get("testRetain"); - WaitForTopicValue watcher = new WaitForTopicValue(embeddedConnection, propertyTestTopic + "/set"); - // Watch the topic. Publish a retain=false value to MQTT - property.getChannelState().publishValue(OnOffType.OFF).get(); - assertThat(watcher.waitForTopicValue(1000), is("false")); - - // Publish a retain=false value to MQTT. - property.getChannelState().publishValue(OnOffType.ON).get(); - // No value is expected to be retained on this MQTT topic - waitForAssert(() -> { - WaitForTopicValue w = new WaitForTopicValue(embeddedConnection, propertyTestTopic + "/set"); - assertNull(w.waitForTopicValue(50)); - }, 500, 100); - } -} diff --git a/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/ThingChannelConstants.java b/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/ThingChannelConstants.java deleted file mode 100644 index b254837c48..0000000000 --- a/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/ThingChannelConstants.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mqtt; - -import static org.openhab.binding.mqtt.homie.generic.internal.MqttBindingConstants.HOMIE300_MQTT_THING; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.thing.ThingUID; - -/** - * Static test definitions, like thing, bridge and channel definitions - * - * @author David Graeff - Initial contribution - */ -@NonNullByDefault -public class ThingChannelConstants { - // Common ThingUID and ChannelUIDs - public final static ThingUID testHomieThing = new ThingUID(HOMIE300_MQTT_THING, "device123"); -} diff --git a/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/homie/HomieImplementationTest.java b/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/homie/HomieImplementationTest.java new file mode 100644 index 0000000000..2ff5f27db9 --- /dev/null +++ b/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/homie/HomieImplementationTest.java @@ -0,0 +1,313 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mqtt.homie; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.binding.mqtt.generic.ChannelState; +import org.openhab.binding.mqtt.generic.tools.ChildMap; +import org.openhab.binding.mqtt.generic.tools.WaitForTopicValue; +import org.openhab.binding.mqtt.homie.internal.handler.HomieThingHandler; +import org.openhab.binding.mqtt.homie.internal.homie300.Device; +import org.openhab.binding.mqtt.homie.internal.homie300.DeviceAttributes; +import org.openhab.binding.mqtt.homie.internal.homie300.DeviceAttributes.ReadyState; +import org.openhab.binding.mqtt.homie.internal.homie300.DeviceCallback; +import org.openhab.binding.mqtt.homie.internal.homie300.Node; +import org.openhab.binding.mqtt.homie.internal.homie300.NodeAttributes; +import org.openhab.binding.mqtt.homie.internal.homie300.Property; +import org.openhab.binding.mqtt.homie.internal.homie300.PropertyAttributes; +import org.openhab.binding.mqtt.homie.internal.homie300.PropertyAttributes.DataTypeEnum; +import org.openhab.binding.mqtt.homie.internal.homie300.PropertyHelper; +import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; +import org.openhab.core.io.transport.mqtt.MqttConnectionObserver; +import org.openhab.core.io.transport.mqtt.MqttConnectionState; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.types.UnDefType; + +/** + * A full implementation test, that starts the embedded MQTT broker and publishes a homie device tree. + * + * @author David Graeff - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class HomieImplementationTest extends MqttOSGiTest { + private static final String BASE_TOPIC = "homie"; + private static final String DEVICE_ID = ThingChannelConstants.TEST_HOME_THING.getId(); + private static final String DEVICE_TOPIC = BASE_TOPIC + "/" + DEVICE_ID; + + private @NonNullByDefault({}) MqttBrokerConnection homieConnection; + private int registeredTopics = 100; + + // The handler is not tested here, so just mock the callback + private @Mock @NonNullByDefault({}) DeviceCallback callback; + + // A handler mock is required to verify that channel value changes have been received + private @Mock @NonNullByDefault({}) HomieThingHandler handler; + + private @NonNullByDefault({}) ScheduledExecutorService scheduler; + + /** + * Create an observer that fails the test as soon as the broker client connection changes its connection state + * to something else then CONNECTED. + */ + private MqttConnectionObserver failIfChange = (state, error) -> assertThat(state, + is(MqttConnectionState.CONNECTED)); + + private String propertyTestTopic = ""; + + @Override + @BeforeEach + public void beforeEach() throws Exception { + super.beforeEach(); + + homieConnection = createBrokerConnection("homie"); + + // If the connection state changes in between -> fail + homieConnection.addConnectionObserver(failIfChange); + + List> futures = new ArrayList<>(); + futures.add(publish(DEVICE_TOPIC + "/$homie", "3.0")); + futures.add(publish(DEVICE_TOPIC + "/$name", "Name")); + futures.add(publish(DEVICE_TOPIC + "/$state", "ready")); + futures.add(publish(DEVICE_TOPIC + "/$nodes", "testnode")); + + // Add homie node topics + final String testNode = DEVICE_TOPIC + "/testnode"; + futures.add(publish(testNode + "/$name", "Testnode")); + futures.add(publish(testNode + "/$type", "Type")); + futures.add(publish(testNode + "/$properties", "temperature,doorbell,testRetain")); + + // Add homie property topics + final String property = testNode + "/temperature"; + futures.add(publish(property, "10")); + futures.add(publish(property + "/$name", "Testprop")); + futures.add(publish(property + "/$settable", "true")); + futures.add(publish(property + "/$unit", "°C")); + futures.add(publish(property + "/$datatype", "float")); + futures.add(publish(property + "/$format", "-100:100")); + + final String propertyBellTopic = testNode + "/doorbell"; + futures.add(publish(propertyBellTopic + "/$name", "Doorbell")); + futures.add(publish(propertyBellTopic + "/$settable", "false")); + futures.add(publish(propertyBellTopic + "/$retained", "false")); + futures.add(publish(propertyBellTopic + "/$datatype", "boolean")); + + this.propertyTestTopic = testNode + "/testRetain"; + futures.add(publish(propertyTestTopic + "/$name", "Test")); + futures.add(publish(propertyTestTopic + "/$settable", "true")); + futures.add(publish(propertyTestTopic + "/$retained", "false")); + futures.add(publish(propertyTestTopic + "/$datatype", "boolean")); + + registeredTopics = futures.size(); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(5, TimeUnit.SECONDS); + + scheduler = new ScheduledThreadPoolExecutor(6); + } + + @Override + @AfterEach + public void afterEach() throws Exception { + if (homieConnection != null) { + homieConnection.removeConnectionObserver(failIfChange); + homieConnection.stop().get(5, TimeUnit.SECONDS); + } + if (scheduler != null) { + scheduler.shutdownNow(); + } + super.afterEach(); + } + + @Test + public void retrieveAllTopics() throws Exception { + // four topics are not under /testnode ! + CountDownLatch c = new CountDownLatch(registeredTopics - 4); + homieConnection.subscribe(DEVICE_TOPIC + "/testnode/#", (topic, payload) -> c.countDown()).get(5, + TimeUnit.SECONDS); + assertTrue(c.await(5, TimeUnit.SECONDS), + "Connection " + homieConnection.getClientId() + " not retrieving all topics "); + } + + @Test + public void retrieveOneAttribute() throws Exception { + WaitForTopicValue watcher = new WaitForTopicValue(homieConnection, DEVICE_TOPIC + "/$homie"); + assertThat(watcher.waitForTopicValue(1000), is("3.0")); + } + + @SuppressWarnings("null") + @Test + public void retrieveAttributes() throws Exception { + assertThat(homieConnection.hasSubscribers(), is(false)); + + Node node = new Node(DEVICE_TOPIC, "testnode", ThingChannelConstants.TEST_HOME_THING, callback, + new NodeAttributes()); + Property property = spy( + new Property(DEVICE_TOPIC + "/testnode", node, "temperature", callback, new PropertyAttributes())); + + // Create a scheduler + ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4); + + property.subscribe(homieConnection, scheduler, 500).get(); + + assertThat(property.attributes.settable, is(true)); + assertThat(property.attributes.retained, is(true)); + assertThat(property.attributes.name, is("Testprop")); + assertThat(property.attributes.unit, is("°C")); + assertThat(property.attributes.datatype, is(DataTypeEnum.float_)); + waitForAssert(() -> assertThat(property.attributes.format, is("-100:100"))); + verify(property, timeout(500).atLeastOnce()).attributesReceived(); + + // Receive property value + ChannelState channelState = spy(property.getChannelState()); + PropertyHelper.setChannelState(property, channelState); + + property.startChannel(homieConnection, scheduler, 500).get(); + verify(channelState).start(any(), any(), anyInt()); + verify(channelState, timeout(500)).processMessage(any(), any()); + verify(callback).updateChannelState(any(), any()); + + assertThat(property.getChannelState().getCache().getChannelState(), + is(new QuantityType<>(10, SIUnits.CELSIUS))); + + property.stop().get(); + assertThat(homieConnection.hasSubscribers(), is(false)); + } + + // Inject a spy'ed property + public Property createSpyProperty(InvocationOnMock invocation) { + final Node node = (Node) invocation.getMock(); + final String id = (String) invocation.getArguments()[0]; + return spy(node.createProperty(id, spy(new PropertyAttributes()))); + } + + // Inject a spy'ed node + public Node createSpyNode(InvocationOnMock invocation) { + final Device device = (Device) invocation.getMock(); + final String id = (String) invocation.getArguments()[0]; + // Create the node + Node node = spy(device.createNode(id, spy(new NodeAttributes()))); + // Intercept creating a property in the next call and inject a spy'ed property. + doAnswer(this::createSpyProperty).when(node).createProperty(any()); + return node; + } + + @SuppressWarnings("null") + @Test + public void parseHomieTree() throws Exception { + // Create a Homie Device object. Because spied Nodes are required for call verification, + // the full Device constructor need to be used and a ChildMap object need to be created manually. + ChildMap nodeMap = new ChildMap<>(); + Device device = spy( + new Device(ThingChannelConstants.TEST_HOME_THING, callback, new DeviceAttributes(), nodeMap)); + + // Intercept creating a node in initialize()->start() and inject a spy'ed node. + doAnswer(this::createSpyNode).when(device).createNode(any()); + + // initialize the device, subscribe and wait. + device.initialize(BASE_TOPIC, DEVICE_ID, Collections.emptyList()); + device.subscribe(homieConnection, scheduler, 1500).get(); + + assertThat(device.isInitialized(), is(true)); + + // Check device attributes + assertThat(device.attributes.homie, is("3.0")); + assertThat(device.attributes.name, is("Name")); + assertThat(device.attributes.state, is(ReadyState.ready)); + assertThat(device.attributes.nodes.length, is(1)); + verify(device, times(4)).attributeChanged(any(), any(), any(), any(), anyBoolean()); + verify(callback).readyStateChanged(eq(ReadyState.ready)); + verify(device).attributesReceived(any(), any(), anyInt()); + + // Expect 1 node + assertThat(device.nodes.size(), is(1)); + + // Check node and node attributes + Node node = device.nodes.get("testnode"); + verify(node).subscribe(any(), any(), anyInt()); + verify(node).attributesReceived(any(), any(), anyInt()); + verify(node.attributes).subscribeAndReceive(any(), any(), anyString(), any(), anyInt()); + assertThat(node.attributes.type, is("Type")); + assertThat(node.attributes.name, is("Testnode")); + + // Expect 2 property + assertThat(node.properties.size(), is(3)); + + // Check property and property attributes + Property property = node.properties.get("temperature"); + assertThat(property.attributes.settable, is(true)); + assertThat(property.attributes.retained, is(true)); + assertThat(property.attributes.name, is("Testprop")); + assertThat(property.attributes.unit, is("°C")); + assertThat(property.attributes.datatype, is(DataTypeEnum.float_)); + assertThat(property.attributes.format, is("-100:100")); + verify(property).attributesReceived(); + assertNotNull(property.getChannelState()); + assertThat(property.getType().getState().getMinimum().intValue(), is(-100)); + assertThat(property.getType().getState().getMaximum().intValue(), is(100)); + + // Check property and property attributes + Property propertyBell = node.properties.get("doorbell"); + verify(propertyBell).attributesReceived(); + assertThat(propertyBell.attributes.settable, is(false)); + assertThat(propertyBell.attributes.retained, is(false)); + assertThat(propertyBell.attributes.name, is("Doorbell")); + assertThat(propertyBell.attributes.datatype, is(DataTypeEnum.boolean_)); + + // The device->node->property tree is ready. Now subscribe to property values. + device.startChannels(homieConnection, scheduler, 50, handler).get(); + assertThat(propertyBell.getChannelState().isStateful(), is(false)); + assertThat(propertyBell.getChannelState().getCache().getChannelState(), is(UnDefType.UNDEF)); + assertThat(property.getChannelState().getCache().getChannelState(), + is(new QuantityType<>(10, SIUnits.CELSIUS))); + + property = node.properties.get("testRetain"); + WaitForTopicValue watcher = new WaitForTopicValue(brokerConnection, propertyTestTopic + "/set"); + // Watch the topic. Publish a retain=false value to MQTT + property.getChannelState().publishValue(OnOffType.OFF).get(); + assertThat(watcher.waitForTopicValue(10000), is("false")); + + // Publish a retain=false value to MQTT. + property.getChannelState().publishValue(OnOffType.ON).get(); + // No value is expected to be retained on this MQTT topic + waitForAssert(() -> { + WaitForTopicValue w = new WaitForTopicValue(brokerConnection, propertyTestTopic + "/set"); + assertNull(w.waitForTopicValue(50)); + }, 500, 100); + } +} diff --git a/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/homie/MqttOSGiTest.java b/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/homie/MqttOSGiTest.java new file mode 100644 index 0000000000..64ae708ad1 --- /dev/null +++ b/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/homie/MqttOSGiTest.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mqtt.homie; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.nio.charset.StandardCharsets; +import java.util.Properties; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; +import org.openhab.core.io.transport.mqtt.MqttConnectionState; +import org.openhab.core.test.java.JavaOSGiTest; + +import io.moquette.BrokerConstants; +import io.moquette.broker.Server; + +/** + * Creates a Moquette MQTT broker instance and a {@link MqttBrokerConnection} for testing MQTT bindings. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class MqttOSGiTest extends JavaOSGiTest { + + private static final String BROKER_ID = "test-broker"; + private static final int BROKER_PORT = Integer.getInteger("mqttbroker.port", 1883); + + protected @NonNullByDefault({}) MqttBrokerConnection brokerConnection; + + private Server moquetteServer = new Server(); + + @BeforeEach + public void beforeEach() throws Exception { + registerVolatileStorageService(); + + moquetteServer = new Server(); + moquetteServer.startServer(brokerProperties()); + + brokerConnection = createBrokerConnection(BROKER_ID); + } + + @AfterEach + public void afterEach() throws Exception { + brokerConnection.stop().get(5, TimeUnit.SECONDS); + moquetteServer.stopServer(); + } + + private Properties brokerProperties() { + Properties properties = new Properties(); + properties.put(BrokerConstants.HOST_PROPERTY_NAME, BrokerConstants.HOST); + properties.put(BrokerConstants.PORT_PROPERTY_NAME, String.valueOf(BROKER_PORT)); + properties.put(BrokerConstants.SSL_PORT_PROPERTY_NAME, BrokerConstants.DISABLED_PORT_BIND); + properties.put(BrokerConstants.WEB_SOCKET_PORT_PROPERTY_NAME, BrokerConstants.DISABLED_PORT_BIND); + properties.put(BrokerConstants.WSS_PORT_PROPERTY_NAME, BrokerConstants.DISABLED_PORT_BIND); + return properties; + } + + protected MqttBrokerConnection createBrokerConnection(String clientId) throws Exception { + MqttBrokerConnection connection = new MqttBrokerConnection(BrokerConstants.HOST, BROKER_PORT, false, clientId); + connection.setQos(1); + connection.start().get(5, TimeUnit.SECONDS); + + waitForAssert(() -> assertThat(connection.connectionState(), is(MqttConnectionState.CONNECTED))); + + return connection; + } + + protected CompletableFuture publish(String topic, String message) { + return brokerConnection.publish(topic, message.getBytes(StandardCharsets.UTF_8), 1, true); + } +} diff --git a/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/homie/ThingChannelConstants.java b/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/homie/ThingChannelConstants.java new file mode 100644 index 0000000000..60646acba2 --- /dev/null +++ b/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/homie/ThingChannelConstants.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mqtt.homie; + +import static org.openhab.binding.mqtt.homie.generic.internal.MqttBindingConstants.HOMIE300_MQTT_THING; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingUID; + +/** + * Static test definitions, like thing, bridge and channel definitions + * + * @author David Graeff - Initial contribution + */ +@NonNullByDefault +public class ThingChannelConstants { + // Common ThingUID and ChannelUIDs + public static final ThingUID TEST_HOME_THING = new ThingUID(HOMIE300_MQTT_THING, "device123"); +} diff --git a/itests/pom.xml b/itests/pom.xml index d5cb682fa9..f58c4d9a7d 100644 --- a/itests/pom.xml +++ b/itests/pom.xml @@ -24,10 +24,8 @@ org.openhab.binding.max.tests org.openhab.binding.mielecloud.tests org.openhab.binding.modbus.tests - + org.openhab.binding.mqtt.homeassistant.tests + org.openhab.binding.mqtt.homie.tests org.openhab.binding.nest.tests org.openhab.binding.ntp.tests org.openhab.binding.systeminfo.tests