topics = new HashSet<>();
@@ -92,12 +94,11 @@ public void processMessage(String topic, byte[] payload) {
HaID haID = new HaID(topic);
String config = new String(payload);
-
AbstractComponent> component = null;
if (config.length() > 0) {
- component = CFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler, gson,
- transformationServiceProvider);
+ component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler,
+ gson, transformationServiceProvider);
}
if (component != null) {
component.setConfigSeen();
@@ -122,9 +123,9 @@ public void processMessage(String topic, byte[] payload) {
* @param connection A MQTT broker connection
* @param discoverTime The time in milliseconds for the discovery to run. Can be 0 to disable the
* timeout.
- * You need to call {@link #stopDiscovery(MqttBrokerConnection)} at some
+ * You need to call {@link #stopDiscovery()} at some
* point in that case.
- * @param topicDescription Contains the object-id (=device id) and potentially a node-id as well.
+ * @param topicDescriptions Contains the object-id (=device id) and potentially a node-id as well.
* @param componentsDiscoveredListener Listener for results
* @return A future that completes normally after the given time in milliseconds or exceptionally on any error.
* Completes immediately if the timeout is disabled.
@@ -177,8 +178,6 @@ private void subscribeSuccess() {
/**
* Stops an ongoing discovery or do nothing if no discovery is running.
- *
- * @param connection A MQTT broker connection
*/
public void stopDiscovery() {
subscribeFail(new Throwable("Stopped"));
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HaID.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HaID.java
index c7f895cd4557e..51a2509205d55 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HaID.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HaID.java
@@ -89,7 +89,7 @@ private HaID(String baseTopic, String objectID, String nodeID, String component)
this.topic = createTopic(this);
}
- private static final String createTopic(HaID id) {
+ private static String createTopic(HaID id) {
StringBuilder str = new StringBuilder();
str.append(id.baseTopic).append('/').append(id.component).append('/');
if (!id.nodeID.isBlank()) {
@@ -104,8 +104,8 @@ private static final String createTopic(HaID id) {
*
* objectid
, nodeid
, and component
values are fetched from the configuration.
*
- * @param baseTopic
- * @param config
+ * @param baseTopic base topic
+ * @param config config
* @return newly created HaID
*/
public static HaID fromConfig(String baseTopic, Configuration config) {
@@ -120,7 +120,7 @@ public static HaID fromConfig(String baseTopic, Configuration config) {
*
* objectid
, nodeid
, and component
values are added to the configuration.
*
- * @param config
+ * @param config config
* @return the modified configuration
*/
public Configuration toConfig(Configuration config) {
@@ -139,7 +139,7 @@ public Configuration toConfig(Configuration config) {
* The component
component in the resulting HaID will be set to +
.
* This enables the HaID to be used as an mqtt subscription topic.
*
- * @param config
+ * @param config config
* @return newly created HaID
*/
public static Collection fromConfig(HandlerConfiguration config) {
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HandlerConfiguration.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HandlerConfiguration.java
index 8c589592e4538..9936d91969389 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HandlerConfiguration.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HandlerConfiguration.java
@@ -28,6 +28,9 @@
*/
@NonNullByDefault
public class HandlerConfiguration {
+ public static final String PROPERTY_BASETOPIC = "basetopic";
+ public static final String PROPERTY_TOPICS = "topics";
+ public static final String DEFAULT_BASETOPIC = "homeassistant";
/**
* hint: cannot be final, or getConfigAs
will not work.
* The MQTT prefix topic
@@ -64,7 +67,7 @@ public class HandlerConfiguration {
public List topics;
public HandlerConfiguration() {
- this("homeassistant", Collections.emptyList());
+ this(DEFAULT_BASETOPIC, Collections.emptyList());
}
public HandlerConfiguration(String basetopic, List topics) {
@@ -76,12 +79,12 @@ public HandlerConfiguration(String basetopic, List topics) {
/**
* Add the basetopic
and objectid
to the properties.
*
- * @param properties
+ * @param properties properties
* @return the modified properties
*/
public > T appendToProperties(T properties) {
- properties.put("basetopic", basetopic);
- properties.put("topics", topics);
+ properties.put(PROPERTY_BASETOPIC, basetopic);
+ properties.put(PROPERTY_TOPICS, topics);
return properties;
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelState.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelState.java
new file mode 100644
index 0000000000000..8ffc0a446b7dd
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelState.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2021 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.internal;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Predicate;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mqtt.generic.ChannelConfig;
+import org.openhab.binding.mqtt.generic.ChannelState;
+import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
+import org.openhab.binding.mqtt.generic.values.Value;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Extended {@link ChannelState} with added filter for {@link #publishValue(Command)}
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+@NonNullByDefault
+public class HomeAssistantChannelState extends ChannelState {
+ private final Logger logger = LoggerFactory.getLogger(HomeAssistantChannelState.class);
+ private final @Nullable Predicate commandFilter;
+
+ /**
+ * Creates a new channel state.
+ *
+ * @param config The channel configuration
+ * @param channelUID The channelUID is used for the {@link ChannelStateUpdateListener} to notify about value changes
+ * @param cachedValue MQTT only notifies us once about a value, during the subscribe. The channel state therefore
+ * needs a cache for the current value.
+ * @param channelStateUpdateListener A channel state update listener
+ * @param commandFilter A filter for commands, on true
command will be published, on
+ * false
ignored. Can be null
to publish all commands.
+ */
+ public HomeAssistantChannelState(ChannelConfig config, ChannelUID channelUID, Value cachedValue,
+ @Nullable ChannelStateUpdateListener channelStateUpdateListener,
+ @Nullable Predicate commandFilter) {
+ super(config, channelUID, cachedValue, channelStateUpdateListener);
+ this.commandFilter = commandFilter;
+ }
+
+ @Override
+ public CompletableFuture publishValue(Command command) {
+ if (commandFilter != null && !commandFilter.test(command)) {
+ logger.trace("Channel {} updates are disabled by command filter, ignoring command {}", channelUID, command);
+ return CompletableFuture.completedFuture(false);
+ }
+ return super.publishValue(command);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractComponent.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponent.java
similarity index 67%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractComponent.java
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponent.java
index 074549d7f8413..6cdde589330db 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractComponent.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponent.java
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.mqtt.homeassistant.internal;
+package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.util.List;
import java.util.Map;
@@ -23,10 +23,14 @@
import org.eclipse.jdt.annotation.Nullable;
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.generic.utils.FutureCollector;
import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
-import org.openhab.binding.mqtt.homeassistant.internal.CFactory.ComponentConfiguration;
+import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
+import org.openhab.binding.mqtt.homeassistant.internal.HaID;
+import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory.ComponentConfiguration;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.type.ChannelDefinition;
@@ -40,10 +44,10 @@
* It has a name and consists of multiple channels.
*
* @author David Graeff - Initial contribution
- * @param Config class derived from {@link BaseChannelConfiguration}
+ * @param Config class derived from {@link AbstractChannelConfiguration}
*/
@NonNullByDefault
-public abstract class AbstractComponent {
+public abstract class AbstractComponent {
// Component location fields
private final ComponentConfiguration componentConfiguration;
protected final ChannelGroupTypeUID channelGroupTypeUID;
@@ -51,7 +55,7 @@ public abstract class AbstractComponent {
protected final HaID haID;
// Channels and configuration
- protected final Map channels = new TreeMap<>();
+ protected final Map channels = new TreeMap<>();
// The hash code ({@link String#hashCode()}) of the configuration string
// Used to determine if a component has changed.
protected final int configHash;
@@ -61,14 +65,12 @@ public abstract class AbstractComponent {
protected boolean configSeen;
/**
- * Provide a thingUID and HomeAssistant topic ID to determine the channel group UID and type.
+ * Creates component based on generic configuration and component configuration type.
*
- * @param thing A ThingUID
- * @param haID A HomeAssistant topic ID
- * @param configJson The configuration string
- * @param gson A Gson instance
+ * @param componentConfiguration generic componentConfiguration with not parsed JSON config
+ * @param clazz target configuration type
*/
- public AbstractComponent(CFactory.ComponentConfiguration componentConfiguration, Class clazz) {
+ public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class clazz) {
this.componentConfiguration = componentConfiguration;
this.channelConfigurationJson = componentConfiguration.getConfigJSON();
@@ -77,24 +79,24 @@ public AbstractComponent(CFactory.ComponentConfiguration componentConfiguration,
this.haID = componentConfiguration.getHaID();
- String groupId = this.haID.getGroupId(channelConfiguration.unique_id);
+ String groupId = this.haID.getGroupId(channelConfiguration.getUniqueId());
this.channelGroupTypeUID = new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, groupId);
this.channelGroupUID = new ChannelGroupUID(componentConfiguration.getThingUID(), groupId);
this.configSeen = false;
- String availability_topic = this.channelConfiguration.availability_topic;
+ String availability_topic = this.channelConfiguration.getAvailabilityTopic();
if (availability_topic != null) {
componentConfiguration.getTracker().addAvailabilityTopic(availability_topic,
- this.channelConfiguration.payload_available, this.channelConfiguration.payload_not_available);
+ this.channelConfiguration.getPayloadAvailable(),
+ this.channelConfiguration.getPayloadNotAvailable());
}
}
- protected CChannel.Builder buildChannel(String channelID, Value valueState, String label,
+ protected ComponentChannel.Builder buildChannel(String channelID, Value valueState, String label,
ChannelStateUpdateListener channelStateUpdateListener) {
- return new CChannel.Builder(this, componentConfiguration, channelID, valueState, label,
- channelStateUpdateListener);
+ return new ComponentChannel.Builder(this, channelID, valueState, label, channelStateUpdateListener);
}
public void setConfigSeen() {
@@ -104,14 +106,15 @@ public void setConfigSeen() {
/**
* Subscribes to all state channels of the component and adds all channels to the provided channel type provider.
*
- * @param connection The connection
- * @param channelStateUpdateListener A listener
+ * @param connection connection to the MQTT broker
+ * @param scheduler thing scheduler
+ * @param timeout channel subscription timeout
* @return A future that completes as soon as all subscriptions have been performed. Completes exceptionally on
* errors.
*/
public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
int timeout) {
- return channels.values().parallelStream().map(v -> v.start(connection, scheduler, timeout))
+ return channels.values().parallelStream().map(cChannel -> cChannel.start(connection, scheduler, timeout))
.collect(FutureCollector.allOf());
}
@@ -122,7 +125,7 @@ public void setConfigSeen() {
* exceptionally on errors.
*/
public CompletableFuture<@Nullable Void> stop() {
- return channels.values().parallelStream().map(CChannel::stop).collect(FutureCollector.allOf());
+ return channels.values().parallelStream().map(ComponentChannel::stop).collect(FutureCollector.allOf());
}
/**
@@ -131,7 +134,7 @@ public void setConfigSeen() {
* @param channelTypeProvider The channel type provider
*/
public void addChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
- channelTypeProvider.setChannelGroupType(groupTypeUID(), type());
+ channelTypeProvider.setChannelGroupType(getGroupTypeUID(), getType());
channels.values().forEach(v -> v.addChannelTypes(channelTypeProvider));
}
@@ -143,46 +146,46 @@ public void addChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
*/
public void removeChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
channels.values().forEach(v -> v.removeChannelTypes(channelTypeProvider));
- channelTypeProvider.removeChannelGroupType(groupTypeUID());
+ channelTypeProvider.removeChannelGroupType(getGroupTypeUID());
}
/**
* Each HomeAssistant component corresponds to a Channel Group Type.
*/
- public ChannelGroupTypeUID groupTypeUID() {
+ public ChannelGroupTypeUID getGroupTypeUID() {
return channelGroupTypeUID;
}
/**
* The unique id of this component.
*/
- public ChannelGroupUID uid() {
+ public ChannelGroupUID getGroupUID() {
return channelGroupUID;
}
/**
* Component (Channel Group) name.
*/
- public String name() {
- return channelConfiguration.name;
+ public String getName() {
+ return channelConfiguration.getName();
}
/**
* Each component consists of multiple Channels.
*/
- public Map channelTypes() {
+ public Map getChannelMap() {
return channels;
}
/**
* Return a components channel. A HomeAssistant MQTT component consists of multiple functions
* and those are mapped to one or more channels. The channel IDs are constants within the
- * derived Component, like the {@link ComponentSwitch#switchChannelID}.
+ * derived Component, like the {@link Switch#switchChannelID}.
*
* @param channelID The channel ID
* @return A components channel
*/
- public @Nullable CChannel channel(String channelID) {
+ public @Nullable ComponentChannel getChannel(String channelID) {
return channels.get(channelID);
}
@@ -196,11 +199,11 @@ public int getConfigHash() {
/**
* Return the channel group type.
*/
- public ChannelGroupType type() {
- final List channelDefinitions = channels.values().stream().map(CChannel::type)
+ public ChannelGroupType getType() {
+ final List channelDefinitions = channels.values().stream().map(ComponentChannel::type)
.collect(Collectors.toList());
- return ChannelGroupTypeBuilder.instance(channelGroupTypeUID, name()).withChannelDefinitions(channelDefinitions)
- .build();
+ return ChannelGroupTypeBuilder.instance(channelGroupTypeUID, getName())
+ .withChannelDefinitions(channelDefinitions).build();
}
/**
@@ -208,13 +211,26 @@ public ChannelGroupType type() {
* to the MQTT broker got lost.
*/
public void resetState() {
- channels.values().forEach(CChannel::resetState);
+ channels.values().forEach(ComponentChannel::resetState);
}
/**
* Return the channel group definition for this component.
*/
public ChannelGroupDefinition getGroupDefinition() {
- return new ChannelGroupDefinition(channelGroupUID.getId(), groupTypeUID(), name(), null);
+ return new ChannelGroupDefinition(channelGroupUID.getId(), getGroupTypeUID(), getName(), null);
+ }
+
+ public HaID getHaID() {
+ return haID;
+ }
+
+ public String getChannelConfigurationJson() {
+ return channelConfigurationJson;
+ }
+
+ @Nullable
+ public TransformationServiceProvider getTransformationServiceProvider() {
+ return componentConfiguration.getTransformationServiceProvider();
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentAlarmControlPanel.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanel.java
similarity index 74%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentAlarmControlPanel.java
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanel.java
index 6ae7d4e0faa79..cd1db7d2d01c9 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentAlarmControlPanel.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanel.java
@@ -10,11 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.mqtt.homeassistant.internal;
+package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.TextValue;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
/**
* A MQTT alarm control panel, following the https://www.home-assistant.io/components/alarm_control_panel.mqtt/
@@ -26,7 +27,7 @@
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
-public class ComponentAlarmControlPanel extends AbstractComponent {
+public class AlarmControlPanel extends AbstractComponent {
public static final String stateChannelID = "alarm"; // Randomly chosen channel "ID"
public static final String switchDisarmChannelID = "disarm"; // Randomly chosen channel "ID"
public static final String switchArmHomeChannelID = "armhome"; // Randomly chosen channel "ID"
@@ -35,7 +36,7 @@ public class ComponentAlarmControlPanel extends AbstractComponent {
+public class BinarySensor extends AbstractComponent {
public static final String sensorChannelID = "sensor"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
- static class ChannelConfiguration extends BaseChannelConfiguration {
+ static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Binary Sensor");
}
@@ -53,16 +54,16 @@ static class ChannelConfiguration extends BaseChannelConfiguration {
protected @Nullable List json_attributes;
}
- public ComponentBinarySensor(CFactory.ComponentConfiguration componentConfiguration) {
+ public BinarySensor(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
OnOffValue value = new OnOffValue(channelConfiguration.payload_on, channelConfiguration.payload_off);
buildChannel(sensorChannelID, value, "value", getListener(componentConfiguration, value))
- .stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template).build();
+ .stateTopic(channelConfiguration.state_topic, channelConfiguration.getValueTemplate()).build();
}
- private ChannelStateUpdateListener getListener(CFactory.ComponentConfiguration componentConfiguration,
+ private ChannelStateUpdateListener getListener(ComponentFactory.ComponentConfiguration componentConfiguration,
Value value) {
ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentCamera.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Camera.java
similarity index 73%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentCamera.java
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Camera.java
index f61cc3bc1ac3c..0b877cf01800c 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentCamera.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Camera.java
@@ -10,10 +10,11 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.mqtt.homeassistant.internal;
+package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mqtt.generic.values.ImageValue;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
/**
* A MQTT camera, following the https://www.home-assistant.io/components/camera.mqtt/ specification.
@@ -23,13 +24,13 @@
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
-public class ComponentCamera extends AbstractComponent {
+public class Camera extends AbstractComponent {
public static final String cameraChannelID = "camera"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
- static class ChannelConfiguration extends BaseChannelConfiguration {
+ static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Camera");
}
@@ -37,12 +38,12 @@ static class ChannelConfiguration extends BaseChannelConfiguration {
protected String topic = "";
}
- public ComponentCamera(CFactory.ComponentConfiguration componentConfiguration) {
+ public Camera(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
ImageValue value = new ImageValue();
- buildChannel(cameraChannelID, value, channelConfiguration.name, componentConfiguration.getUpdateListener())
+ buildChannel(cameraChannelID, value, channelConfiguration.getName(), componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.topic).build();
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java
new file mode 100644
index 0000000000000..c030d8a27e081
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java
@@ -0,0 +1,237 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.component;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Predicate;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
+import org.openhab.binding.mqtt.generic.values.NumberValue;
+import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.binding.mqtt.generic.values.TextValue;
+import org.openhab.binding.mqtt.generic.values.Value;
+import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+
+/**
+ * A MQTT climate component, following the https://www.home-assistant.io/components/climate.mqtt/ specification.
+ *
+ * @author David Graeff - Initial contribution
+ * @author Anton Kharuzhy - Implementation
+ */
+@NonNullByDefault
+public class Climate extends AbstractComponent {
+ public static final String ACTION_CH_ID = "action";
+ public static final String AUX_CH_ID = "aux";
+ public static final String AWAY_MODE_CH_ID = "awayMode";
+ public static final String CURRENT_TEMPERATURE_CH_ID = "currentTemperature";
+ public static final String FAN_MODE_CH_ID = "fanMode";
+ public static final String HOLD_CH_ID = "hold";
+ public static final String MODE_CH_ID = "mode";
+ public static final String SWING_CH_ID = "swing";
+ public static final String TEMPERATURE_CH_ID = "temperature";
+ public static final String TEMPERATURE_HIGH_CH_ID = "temperatureHigh";
+ public static final String TEMPERATURE_LOW_CH_ID = "temperatureLow";
+ public static final String POWER_CH_ID = "power";
+
+ private static final String CELSIUM = "C";
+ private static final String FAHRENHEIT = "F";
+ private static final float DEFAULT_CELSIUM_PRECISION = 0.1f;
+ private static final float DEFAULT_FAHRENHEIT_PRECISION = 1f;
+
+ private static final String ACTION_OFF = "off";
+ private static final State ACTION_OFF_STATE = new StringType(ACTION_OFF);
+ private static final List ACTION_MODES = List.of(ACTION_OFF, "heating", "cooling", "drying", "idle", "fan");
+
+ /**
+ * Configuration class for MQTT component
+ */
+ static class ChannelConfiguration extends AbstractChannelConfiguration {
+ ChannelConfiguration() {
+ super("MQTT HVAC");
+ }
+
+ protected @Nullable String action_template;
+ protected @Nullable String action_topic;
+
+ protected @Nullable String aux_command_topic;
+ protected @Nullable String aux_state_template;
+ protected @Nullable String aux_state_topic;
+
+ protected @Nullable String away_mode_command_topic;
+ protected @Nullable String away_mode_state_template;
+ protected @Nullable String away_mode_state_topic;
+
+ protected @Nullable String current_temperature_template;
+ protected @Nullable String current_temperature_topic;
+
+ protected @Nullable String fan_mode_command_template;
+ protected @Nullable String fan_mode_command_topic;
+ protected @Nullable String fan_mode_state_template;
+ protected @Nullable String fan_mode_state_topic;
+ protected List fan_modes = Arrays.asList("auto", "low", "medium", "high");
+
+ protected @Nullable String hold_command_template;
+ protected @Nullable String hold_command_topic;
+ protected @Nullable String hold_state_template;
+ protected @Nullable String hold_state_topic;
+ protected @Nullable List hold_modes; // Are there default modes? Now the channel will be ignored without
+ // hold modes.
+
+ protected @Nullable String json_attributes_template; // Attributes are not supported yet
+ protected @Nullable String json_attributes_topic;
+
+ protected @Nullable String mode_command_template;
+ protected @Nullable String mode_command_topic;
+ protected @Nullable String mode_state_template;
+ protected @Nullable String mode_state_topic;
+ protected List modes = Arrays.asList("auto", "off", "cool", "heat", "dry", "fan_only");
+
+ protected @Nullable String swing_command_template;
+ protected @Nullable String swing_command_topic;
+ protected @Nullable String swing_state_template;
+ protected @Nullable String swing_state_topic;
+ protected List swing_modes = Arrays.asList("on", "off");
+
+ protected @Nullable String temperature_command_template;
+ protected @Nullable String temperature_command_topic;
+ protected @Nullable String temperature_state_template;
+ protected @Nullable String temperature_state_topic;
+
+ protected @Nullable String temperature_high_command_template;
+ protected @Nullable String temperature_high_command_topic;
+ protected @Nullable String temperature_high_state_template;
+ protected @Nullable String temperature_high_state_topic;
+
+ protected @Nullable String temperature_low_command_template;
+ protected @Nullable String temperature_low_command_topic;
+ protected @Nullable String temperature_low_state_template;
+ protected @Nullable String temperature_low_state_topic;
+
+ protected @Nullable String power_command_topic;
+
+ protected Integer initial = 21;
+ protected @Nullable Float max_temp;
+ protected @Nullable Float min_temp;
+ protected String temperature_unit = CELSIUM; // System unit by default
+ protected Float temp_step = 1f;
+ protected @Nullable Float precision;
+ protected Boolean send_if_off = true;
+ }
+
+ public Climate(ComponentFactory.ComponentConfiguration componentConfiguration) {
+ super(componentConfiguration, ChannelConfiguration.class);
+
+ BigDecimal minTemp = channelConfiguration.min_temp != null ? BigDecimal.valueOf(channelConfiguration.min_temp)
+ : null;
+ BigDecimal maxTemp = channelConfiguration.max_temp != null ? BigDecimal.valueOf(channelConfiguration.max_temp)
+ : null;
+ float precision = channelConfiguration.precision != null ? channelConfiguration.precision
+ : (FAHRENHEIT.equals(channelConfiguration.temperature_unit) ? DEFAULT_FAHRENHEIT_PRECISION
+ : DEFAULT_CELSIUM_PRECISION);
+ final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
+
+ ComponentChannel actionChannel = buildOptionalChannel(ACTION_CH_ID,
+ new TextValue(ACTION_MODES.toArray(new String[0])), updateListener, null, null,
+ channelConfiguration.action_template, channelConfiguration.action_topic, null);
+
+ final Predicate commandFilter = channelConfiguration.send_if_off ? null
+ : getCommandFilter(actionChannel);
+
+ buildOptionalChannel(AUX_CH_ID, new OnOffValue(), updateListener, null, channelConfiguration.aux_command_topic,
+ channelConfiguration.aux_state_template, channelConfiguration.aux_state_topic, commandFilter);
+
+ buildOptionalChannel(AWAY_MODE_CH_ID, new OnOffValue(), updateListener, null,
+ channelConfiguration.away_mode_command_topic, channelConfiguration.away_mode_state_template,
+ channelConfiguration.away_mode_state_topic, commandFilter);
+
+ buildOptionalChannel(CURRENT_TEMPERATURE_CH_ID,
+ new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(precision), channelConfiguration.temperature_unit),
+ updateListener, null, null, channelConfiguration.current_temperature_template,
+ channelConfiguration.current_temperature_topic, commandFilter);
+
+ buildOptionalChannel(FAN_MODE_CH_ID, new TextValue(channelConfiguration.fan_modes.toArray(new String[0])),
+ updateListener, channelConfiguration.fan_mode_command_template,
+ channelConfiguration.fan_mode_command_topic, channelConfiguration.fan_mode_state_template,
+ channelConfiguration.fan_mode_state_topic, commandFilter);
+
+ if (channelConfiguration.hold_modes != null && !channelConfiguration.hold_modes.isEmpty()) {
+ buildOptionalChannel(HOLD_CH_ID, new TextValue(channelConfiguration.hold_modes.toArray(new String[0])),
+ updateListener, channelConfiguration.hold_command_template, channelConfiguration.hold_command_topic,
+ channelConfiguration.hold_state_template, channelConfiguration.hold_state_topic, commandFilter);
+ }
+
+ buildOptionalChannel(MODE_CH_ID, new TextValue(channelConfiguration.modes.toArray(new String[0])),
+ updateListener, channelConfiguration.mode_command_template, channelConfiguration.mode_command_topic,
+ channelConfiguration.mode_state_template, channelConfiguration.mode_state_topic, commandFilter);
+
+ buildOptionalChannel(SWING_CH_ID, new TextValue(channelConfiguration.swing_modes.toArray(new String[0])),
+ updateListener, channelConfiguration.swing_command_template, channelConfiguration.swing_command_topic,
+ channelConfiguration.swing_state_template, channelConfiguration.swing_state_topic, commandFilter);
+
+ buildOptionalChannel(TEMPERATURE_CH_ID,
+ new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(channelConfiguration.temp_step),
+ channelConfiguration.temperature_unit),
+ updateListener, channelConfiguration.temperature_command_template,
+ channelConfiguration.temperature_command_topic, channelConfiguration.temperature_state_template,
+ channelConfiguration.temperature_state_topic, commandFilter);
+
+ buildOptionalChannel(TEMPERATURE_HIGH_CH_ID,
+ new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(channelConfiguration.temp_step),
+ channelConfiguration.temperature_unit),
+ updateListener, channelConfiguration.temperature_high_command_template,
+ channelConfiguration.temperature_high_command_topic,
+ channelConfiguration.temperature_high_state_template, channelConfiguration.temperature_high_state_topic,
+ commandFilter);
+
+ buildOptionalChannel(TEMPERATURE_LOW_CH_ID,
+ new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(channelConfiguration.temp_step),
+ channelConfiguration.temperature_unit),
+ updateListener, channelConfiguration.temperature_low_command_template,
+ channelConfiguration.temperature_low_command_topic, channelConfiguration.temperature_low_state_template,
+ channelConfiguration.temperature_low_state_topic, commandFilter);
+
+ buildOptionalChannel(POWER_CH_ID, new OnOffValue(), updateListener, null,
+ channelConfiguration.power_command_topic, null, null, null);
+ }
+
+ @Nullable
+ private ComponentChannel buildOptionalChannel(String channelId, Value valueState,
+ ChannelStateUpdateListener channelStateUpdateListener, @Nullable String commandTemplate,
+ @Nullable String commandTopic, @Nullable String stateTemplate, @Nullable String stateTopic,
+ @Nullable Predicate commandFilter) {
+ if ((commandTopic != null && !commandTopic.isBlank()) || (stateTopic != null && !stateTopic.isBlank())) {
+ return buildChannel(channelId, valueState, channelConfiguration.getName(), channelStateUpdateListener)
+ .stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
+ .commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
+ commandTemplate)
+ .commandFilter(commandFilter).build();
+ }
+ return null;
+ }
+
+ private @Nullable Predicate getCommandFilter(@Nullable ComponentChannel actionChannel) {
+ if (actionChannel == null) {
+ return null;
+ }
+ final var val = actionChannel.getState().getCache();
+ return command -> !ACTION_OFF_STATE.equals(val.getChannelState());
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/CFactory.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java
similarity index 76%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/CFactory.java
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java
index fc3d43a6ee276..ed7b530ba9908 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/CFactory.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.mqtt.homeassistant.internal;
+package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.util.concurrent.ScheduledExecutorService;
@@ -19,6 +19,8 @@
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.HaID;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,8 +34,8 @@
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
-public class CFactory {
- private static final Logger logger = LoggerFactory.getLogger(CFactory.class);
+public class ComponentFactory {
+ private static final Logger logger = LoggerFactory.getLogger(ComponentFactory.class);
/**
* Create a HA MQTT component. The configuration JSon string is required.
@@ -41,7 +43,7 @@ public class CFactory {
* @param thingUID The Thing UID that this component will belong to.
* @param haID The location of this component. The HomeAssistant ID contains the object-id, node-id and
* component-id.
- * @param configJSON Most components expect a "name", a "state_topic" and "command_topic" like with
+ * @param channelConfigurationJSON Most components expect a "name", a "state_topic" and "command_topic" like with
* "{name:'Name',state_topic:'homeassistant/switch/0/object/state',command_topic:'homeassistant/switch/0/object/set'".
* @param updateListener A channel state update listener
* @return A HA MQTT Component
@@ -56,25 +58,25 @@ public class CFactory {
try {
switch (haID.component) {
case "alarm_control_panel":
- return new ComponentAlarmControlPanel(componentConfiguration);
+ return new AlarmControlPanel(componentConfiguration);
case "binary_sensor":
- return new ComponentBinarySensor(componentConfiguration);
+ return new BinarySensor(componentConfiguration);
case "camera":
- return new ComponentCamera(componentConfiguration);
+ return new Camera(componentConfiguration);
case "cover":
- return new ComponentCover(componentConfiguration);
+ return new Cover(componentConfiguration);
case "fan":
- return new ComponentFan(componentConfiguration);
+ return new Fan(componentConfiguration);
case "climate":
- return new ComponentClimate(componentConfiguration);
+ return new Climate(componentConfiguration);
case "light":
- return new ComponentLight(componentConfiguration);
+ return new Light(componentConfiguration);
case "lock":
- return new ComponentLock(componentConfiguration);
+ return new Lock(componentConfiguration);
case "sensor":
- return new ComponentSensor(componentConfiguration);
+ return new Sensor(componentConfiguration);
case "switch":
- return new ComponentSwitch(componentConfiguration);
+ return new Switch(componentConfiguration);
}
} catch (UnsupportedOperationException e) {
logger.warn("Not supported", e);
@@ -92,6 +94,14 @@ protected static class ComponentConfiguration {
private final ScheduledExecutorService scheduler;
private @Nullable TransformationServiceProvider transformationServiceProvider;
+ /**
+ * Provide a thingUID and HomeAssistant topic ID to determine the channel group UID and type.
+ *
+ * @param thingUID A ThingUID
+ * @param haID A HomeAssistant topic ID
+ * @param configJSON The configuration string
+ * @param gson A Gson instance
+ */
protected ComponentConfiguration(ThingUID thingUID, HaID haID, String configJSON, Gson gson,
ChannelStateUpdateListener updateListener, AvailabilityTracker tracker,
ScheduledExecutorService scheduler) {
@@ -143,8 +153,8 @@ public ScheduledExecutorService getScheduler() {
return scheduler;
}
- public C getConfig(Class clazz) {
- return BaseChannelConfiguration.fromString(configJSON, gson, clazz);
+ public C getConfig(Class clazz) {
+ return AbstractChannelConfiguration.fromString(configJSON, gson, clazz);
}
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentCover.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Cover.java
similarity index 73%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentCover.java
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Cover.java
index e2c89b01e732d..bf7af7b942ca0 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentCover.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Cover.java
@@ -10,11 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.mqtt.homeassistant.internal;
+package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.RollershutterValue;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
/**
* A MQTT Cover component, following the https://www.home-assistant.io/components/cover.mqtt/ specification.
@@ -24,13 +25,13 @@
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
-public class ComponentCover extends AbstractComponent {
+public class Cover extends AbstractComponent {
public static final String switchChannelID = "cover"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
- static class ChannelConfiguration extends BaseChannelConfiguration {
+ static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Cover");
}
@@ -42,14 +43,16 @@ static class ChannelConfiguration extends BaseChannelConfiguration {
protected String payload_stop = "STOP";
}
- public ComponentCover(CFactory.ComponentConfiguration componentConfiguration) {
+ public Cover(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
RollershutterValue value = new RollershutterValue(channelConfiguration.payload_open,
channelConfiguration.payload_close, channelConfiguration.payload_stop);
- buildChannel(switchChannelID, value, channelConfiguration.name, componentConfiguration.getUpdateListener())
- .stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)
- .commandTopic(channelConfiguration.command_topic, channelConfiguration.retain).build();
+ buildChannel(switchChannelID, value, channelConfiguration.getName(), componentConfiguration.getUpdateListener())
+ .stateTopic(channelConfiguration.state_topic, channelConfiguration.getValueTemplate())
+ .commandTopic(channelConfiguration.command_topic, channelConfiguration.isRetain(),
+ channelConfiguration.getQos())
+ .build();
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentFan.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Fan.java
similarity index 71%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentFan.java
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Fan.java
index 069585b747ab2..9f74032fa167b 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentFan.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Fan.java
@@ -10,11 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.mqtt.homeassistant.internal;
+package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
/**
* A MQTT Fan component, following the https://www.home-assistant.io/components/fan.mqtt/ specification.
@@ -24,13 +25,13 @@
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
-public class ComponentFan extends AbstractComponent {
+public class Fan extends AbstractComponent {
public static final String switchChannelID = "fan"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
- static class ChannelConfiguration extends BaseChannelConfiguration {
+ static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Fan");
}
@@ -41,12 +42,14 @@ static class ChannelConfiguration extends BaseChannelConfiguration {
protected String payload_off = "OFF";
}
- public ComponentFan(CFactory.ComponentConfiguration componentConfiguration) {
+ public Fan(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
OnOffValue value = new OnOffValue(channelConfiguration.payload_on, channelConfiguration.payload_off);
- buildChannel(switchChannelID, value, channelConfiguration.name, componentConfiguration.getUpdateListener())
- .stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)
- .commandTopic(channelConfiguration.command_topic, channelConfiguration.retain).build();
+ buildChannel(switchChannelID, value, channelConfiguration.getName(), componentConfiguration.getUpdateListener())
+ .stateTopic(channelConfiguration.state_topic, channelConfiguration.getValueTemplate())
+ .commandTopic(channelConfiguration.command_topic, channelConfiguration.isRetain(),
+ channelConfiguration.getQos())
+ .build();
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentLight.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Light.java
similarity index 84%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentLight.java
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Light.java
index e42ed29bbce47..cf7f962c862d2 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentLight.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Light.java
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.mqtt.homeassistant.internal;
+package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -22,6 +22,8 @@
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.mapping.ColorMode;
import org.openhab.binding.mqtt.generic.values.ColorValue;
+import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.Command;
@@ -36,8 +38,7 @@
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
-public class ComponentLight extends AbstractComponent
- implements ChannelStateUpdateListener {
+public class Light extends AbstractComponent implements ChannelStateUpdateListener {
public static final String switchChannelID = "light"; // Randomly chosen channel "ID"
public static final String brightnessChannelID = "brightness"; // Randomly chosen channel "ID"
public static final String colorChannelID = "color"; // Randomly chosen channel "ID"
@@ -45,7 +46,7 @@ public class ComponentLight extends AbstractComponent {
+public class Lock extends AbstractComponent {
public static final String switchChannelID = "lock"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
- static class ChannelConfiguration extends BaseChannelConfiguration {
+ static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Lock");
}
@@ -41,7 +42,7 @@ static class ChannelConfiguration extends BaseChannelConfiguration {
protected @Nullable String command_topic;
}
- public ComponentLock(CFactory.ComponentConfiguration componentConfiguration) {
+ public Lock(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
// We do not support all HomeAssistant quirks
@@ -51,8 +52,10 @@ public ComponentLock(CFactory.ComponentConfiguration componentConfiguration) {
buildChannel(switchChannelID,
new OnOffValue(channelConfiguration.payload_lock, channelConfiguration.payload_unlock),
- channelConfiguration.name, componentConfiguration.getUpdateListener())
- .stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)
- .commandTopic(channelConfiguration.command_topic, channelConfiguration.retain).build();
+ channelConfiguration.getName(), componentConfiguration.getUpdateListener())
+ .stateTopic(channelConfiguration.state_topic, channelConfiguration.getValueTemplate())
+ .commandTopic(channelConfiguration.command_topic, channelConfiguration.isRetain(),
+ channelConfiguration.getQos())
+ .build();
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentSensor.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Sensor.java
similarity index 80%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentSensor.java
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Sensor.java
index 2e18a3a2dd6f1..43bbf4a7a1843 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentSensor.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Sensor.java
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.mqtt.homeassistant.internal;
+package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.util.List;
import java.util.regex.Pattern;
@@ -21,6 +21,7 @@
import org.openhab.binding.mqtt.generic.values.NumberValue;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.generic.values.Value;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.listener.ExpireUpdateStateListener;
/**
@@ -29,14 +30,14 @@
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
-public class ComponentSensor extends AbstractComponent {
+public class Sensor extends AbstractComponent {
public static final String sensorChannelID = "sensor"; // Randomly chosen channel "ID"
private static final Pattern triggerIcons = Pattern.compile("^mdi:(toggle|gesture).*$");
/**
* Configuration class for MQTT component
*/
- static class ChannelConfiguration extends BaseChannelConfiguration {
+ static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Sensor");
}
@@ -53,11 +54,10 @@ static class ChannelConfiguration extends BaseChannelConfiguration {
protected @Nullable List json_attributes;
}
- public ComponentSensor(CFactory.ComponentConfiguration componentConfiguration) {
+ public Sensor(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
Value value;
-
String uom = channelConfiguration.unit_of_measurement;
if (uom != null && !uom.isBlank()) {
@@ -66,16 +66,16 @@ public ComponentSensor(CFactory.ComponentConfiguration componentConfiguration) {
value = new TextValue();
}
- String icon = channelConfiguration.icon;
+ String icon = channelConfiguration.getIcon();
boolean trigger = triggerIcons.matcher(icon).matches();
- buildChannel(sensorChannelID, value, channelConfiguration.name, getListener(componentConfiguration, value))
- .stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)//
+ buildChannel(sensorChannelID, value, channelConfiguration.getName(), getListener(componentConfiguration, value))
+ .stateTopic(channelConfiguration.state_topic, channelConfiguration.getValueTemplate())//
.trigger(trigger).build();
}
- private ChannelStateUpdateListener getListener(CFactory.ComponentConfiguration componentConfiguration,
+ private ChannelStateUpdateListener getListener(ComponentFactory.ComponentConfiguration componentConfiguration,
Value value) {
ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentSwitch.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Switch.java
similarity index 82%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentSwitch.java
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Switch.java
index 87f179d132b27..6d9996f035ea9 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentSwitch.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Switch.java
@@ -10,11 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.mqtt.homeassistant.internal;
+package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
/**
* A MQTT switch, following the https://www.home-assistant.io/components/switch.mqtt/ specification.
@@ -22,13 +23,13 @@
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
-public class ComponentSwitch extends AbstractComponent {
+public class Switch extends AbstractComponent {
public static final String switchChannelID = "switch"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
- static class ChannelConfiguration extends BaseChannelConfiguration {
+ static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Switch");
}
@@ -47,7 +48,7 @@ static class ChannelConfiguration extends BaseChannelConfiguration {
protected @Nullable String json_attributes_template;
}
- public ComponentSwitch(CFactory.ComponentConfiguration componentConfiguration) {
+ public Switch(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
@@ -66,8 +67,9 @@ public ComponentSwitch(CFactory.ComponentConfiguration componentConfiguration) {
channelConfiguration.payload_off);
buildChannel(switchChannelID, value, "state", componentConfiguration.getUpdateListener())
- .stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)
- .commandTopic(channelConfiguration.command_topic, channelConfiguration.retain, channelConfiguration.qos)
+ .stateTopic(channelConfiguration.state_topic, channelConfiguration.getValueTemplate())
+ .commandTopic(channelConfiguration.command_topic, channelConfiguration.isRetain(),
+ channelConfiguration.getQos())
.build();
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ChannelConfigurationTypeAdapterFactory.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ChannelConfigurationTypeAdapterFactory.java
similarity index 81%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ChannelConfigurationTypeAdapterFactory.java
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ChannelConfigurationTypeAdapterFactory.java
index 5d61e981aeb87..ab4d08efb23fe 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ChannelConfigurationTypeAdapterFactory.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ChannelConfigurationTypeAdapterFactory.java
@@ -10,13 +10,16 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.mqtt.homeassistant.internal;
+package org.openhab.binding.mqtt.homeassistant.internal.config;
import java.io.IOException;
import java.lang.reflect.Field;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mqtt.homeassistant.internal.MappingJsonReader;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
@@ -28,12 +31,16 @@
/**
* This a Gson type adapter factory.
*
- * It will create a type adapter for every class derived from {@link BaseChannelConfiguration} and ensures,
+ *
+ * It will create a type adapter for every class derived from {@link
+ * AbstractChannelConfiguration} and ensures,
* that abbreviated names are replaces with their long versions during the read.
*
+ *
* In elements, whose name end in'_topic' '~' replacement is performed.
*
- * The adapters also handle {@link BaseChannelConfiguration.Device}
+ *
+ * The adapters also handle {@link Device}
*
* @author Jochen Klein - Initial contribution
*/
@@ -46,21 +53,22 @@ public TypeAdapter create(@Nullable Gson gson, @Nullable TypeToken typ
if (gson == null || type == null) {
return null;
}
- if (BaseChannelConfiguration.class.isAssignableFrom(type.getRawType())) {
+ if (AbstractChannelConfiguration.class.isAssignableFrom(type.getRawType())) {
return createHAConfig(gson, type);
}
- if (BaseChannelConfiguration.Device.class.isAssignableFrom(type.getRawType())) {
+ if (Device.class.isAssignableFrom(type.getRawType())) {
return createHADevice(gson, type);
}
return null;
}
/**
- * Handle {@link BaseChannelConfiguration}
+ * Handle {@link
+ * AbstractChannelConfiguration}
*
- * @param gson
- * @param type
- * @return
+ * @param gson parser
+ * @param type type
+ * @return adapter
*/
private TypeAdapter createHAConfig(Gson gson, TypeToken type) {
/* The delegate is the 'default' adapter */
@@ -72,7 +80,7 @@ private TypeAdapter createHAConfig(Gson gson, TypeToken type) {
/* read the object using the default adapter, but translate the names in the reader */
T result = delegate.read(MappingJsonReader.getConfigMapper(in));
/* do the '~' expansion afterwards */
- expandTidleInTopics(BaseChannelConfiguration.class.cast(result));
+ expandTidleInTopics(AbstractChannelConfiguration.class.cast(result));
return result;
}
@@ -102,10 +110,10 @@ public void write(JsonWriter out, @Nullable T value) throws IOException {
};
}
- private void expandTidleInTopics(BaseChannelConfiguration config) {
+ private void expandTidleInTopics(AbstractChannelConfiguration config) {
Class> type = config.getClass();
- String tilde = config.tilde;
+ String tilde = config.getTilde();
while (type != Object.class) {
Field[] fields = type.getDeclaredFields();
@@ -127,9 +135,7 @@ private void expandTidleInTopics(BaseChannelConfiguration config) {
}
field.set(config, newValue);
- } catch (IllegalArgumentException e) {
- throw new RuntimeException(e);
- } catch (IllegalAccessException e) {
+ } catch (IllegalArgumentException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ConnectionDeserializer.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ConnectionDeserializer.java
similarity index 66%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ConnectionDeserializer.java
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ConnectionDeserializer.java
index 7383b4d7c4277..405931b790e55 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ConnectionDeserializer.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ConnectionDeserializer.java
@@ -10,10 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.mqtt.homeassistant.internal;
+package org.openhab.binding.mqtt.homeassistant.internal.config;
import java.lang.reflect.Type;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Connection;
+
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
@@ -27,14 +29,11 @@
*
* @author Jan N. Klug - Initial contribution
*/
-public class ConnectionDeserializer implements JsonDeserializer {
+public class ConnectionDeserializer implements JsonDeserializer {
@Override
- public BaseChannelConfiguration.Connection deserialize(JsonElement json, Type typeOfT,
- JsonDeserializationContext context) throws JsonParseException {
+ public Connection deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
JsonArray list = json.getAsJsonArray();
- BaseChannelConfiguration.Connection conn = new BaseChannelConfiguration.Connection();
- conn.type = list.get(0).getAsString();
- conn.identifier = list.get(1).getAsString();
- return conn;
+ return new Connection(list.get(0).getAsString(), list.get(1).getAsString());
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ListOrStringDeserializer.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ListOrStringDeserializer.java
similarity index 93%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ListOrStringDeserializer.java
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ListOrStringDeserializer.java
index d848b1d33d302..94320c1d49acb 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ListOrStringDeserializer.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ListOrStringDeserializer.java
@@ -10,11 +10,11 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.mqtt.homeassistant.internal;
+package org.openhab.binding.mqtt.homeassistant.internal.config;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -62,7 +62,7 @@ public void write(@Nullable JsonWriter out, @Nullable List value) throws
in.nextNull();
return null;
case STRING:
- return Arrays.asList(in.nextString());
+ return Collections.singletonList(in.nextString());
case BEGIN_ARRAY:
return readList(in);
default:
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/BaseChannelConfiguration.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/AbstractChannelConfiguration.java
similarity index 59%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/BaseChannelConfiguration.java
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/AbstractChannelConfiguration.java
index bb17b03530cf1..daa86aa38cd87 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/BaseChannelConfiguration.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/AbstractChannelConfiguration.java
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.mqtt.homeassistant.internal;
+package org.openhab.binding.mqtt.homeassistant.internal.config.dto;
import java.util.List;
import java.util.Map;
@@ -22,7 +22,6 @@
import org.openhab.core.util.UIDUtils;
import com.google.gson.Gson;
-import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
/**
@@ -31,44 +30,8 @@
* @author Jochen Klein - Initial contribution
*/
@NonNullByDefault
-public abstract class BaseChannelConfiguration {
-
- /**
- * This class is needed, to be able to parse only the common base attributes.
- * Without this, {@link BaseChannelConfiguration} cannot be instantiated, as it is abstract.
- * This is needed during the discovery.
- */
- private static class Config extends BaseChannelConfiguration {
- public Config() {
- super("private");
- }
- }
-
- /**
- * Parse the configJSON into a subclass of {@link BaseChannelConfiguration}
- *
- * @param configJSON
- * @param gson
- * @param clazz
- * @return configuration object
- */
- public static C fromString(final String configJSON, final Gson gson,
- final Class clazz) {
- return Objects.requireNonNull(gson.fromJson(configJSON, clazz));
- }
-
- /**
- * Parse the base properties of the configJSON into a {@link BaseChannelConfiguration}
- *
- * @param configJSON
- * @param gson
- * @return configuration object
- */
- public static BaseChannelConfiguration fromString(final String configJSON, final Gson gson) {
- return fromString(configJSON, gson, Config.class);
- }
-
- public String name;
+public abstract class AbstractChannelConfiguration {
+ protected String name;
protected String icon = "";
protected int qos; // defaults to 0 according to HA specification
@@ -76,46 +39,42 @@ public static BaseChannelConfiguration fromString(final String configJSON, final
protected @Nullable String value_template;
protected @Nullable String unique_id;
+ protected AvailabilityMode availability_mode = AvailabilityMode.LATEST;
protected @Nullable String availability_topic;
protected String payload_available = "online";
protected String payload_not_available = "offline";
+ /**
+ * A list of MQTT topics subscribed to receive availability (online/offline) updates. Must not be used together with
+ * availability_topic
+ */
+ protected @Nullable List availability;
+
@SerializedName(value = "~")
protected String tilde = "";
- protected BaseChannelConfiguration(String defaultName) {
- this.name = defaultName;
- }
+ protected @Nullable Device device;
- public @Nullable String expand(@Nullable String value) {
- return value == null ? null : value.replaceAll("~", tilde);
+ /**
+ * Parse the base properties of the configJSON into a {@link AbstractChannelConfiguration}
+ *
+ * @param configJSON channels configuration in JSON
+ * @param gson parser
+ * @return configuration object
+ */
+ public static AbstractChannelConfiguration fromString(final String configJSON, final Gson gson) {
+ return fromString(configJSON, gson, Config.class);
}
- protected @Nullable Device device;
-
- static class Device {
- @JsonAdapter(ListOrStringDeserializer.class)
- protected @Nullable List identifiers;
- protected @Nullable List connections;
- protected @Nullable String manufacturer;
- protected @Nullable String model;
- protected @Nullable String name;
- protected @Nullable String sw_version;
-
- public @Nullable String getId() {
- List identifiers = this.identifiers;
- return identifiers == null ? null : String.join("_", identifiers);
- }
+ protected AbstractChannelConfiguration(String defaultName) {
+ this.name = defaultName;
}
- @JsonAdapter(ConnectionDeserializer.class)
- static class Connection {
- protected @Nullable String type;
- protected @Nullable String identifier;
+ public @Nullable String expand(@Nullable String value) {
+ return value == null ? null : value.replaceAll("~", tilde);
}
public String getThingName() {
- @Nullable
String result = null;
if (this.device != null) {
@@ -128,7 +87,6 @@ public String getThingName() {
}
public String getThingId(String defaultId) {
- @Nullable
String result = null;
if (this.device != null) {
result = this.device.getId();
@@ -152,10 +110,91 @@ public Map appendToProperties(Map properties) {
if (model != null) {
properties.put(Thing.PROPERTY_MODEL_ID, model);
}
- final String sw_version = device_.sw_version;
+ final String sw_version = device_.swVersion;
if (sw_version != null) {
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, sw_version);
}
return properties;
}
+
+ public String getName() {
+ return name;
+ }
+
+ public String getIcon() {
+ return icon;
+ }
+
+ public int getQos() {
+ return qos;
+ }
+
+ public boolean isRetain() {
+ return retain;
+ }
+
+ @Nullable
+ public String getValueTemplate() {
+ return value_template;
+ }
+
+ @Nullable
+ public String getUniqueId() {
+ return unique_id;
+ }
+
+ @Nullable
+ public String getAvailabilityTopic() {
+ return availability_topic;
+ }
+
+ public String getPayloadAvailable() {
+ return payload_available;
+ }
+
+ public String getPayloadNotAvailable() {
+ return payload_not_available;
+ }
+
+ @Nullable
+ public Device getDevice() {
+ return device;
+ }
+
+ @Nullable
+ public List getAvailability() {
+ return availability;
+ }
+
+ public String getTilde() {
+ return tilde;
+ }
+
+ public AvailabilityMode getAvailabilityMode() {
+ return availability_mode;
+ }
+
+ /**
+ * This class is needed, to be able to parse only the common base attributes.
+ * Without this, {@link AbstractChannelConfiguration} cannot be instantiated, as it is abstract.
+ * This is needed during the discovery.
+ */
+ private static class Config extends AbstractChannelConfiguration {
+ public Config() {
+ super("private");
+ }
+ }
+
+ /**
+ * Parse the configJSON into a subclass of {@link AbstractChannelConfiguration}
+ *
+ * @param configJSON channels configuration in JSON
+ * @param gson parser
+ * @param clazz target configuration class
+ * @return configuration object
+ */
+ public static C fromString(final String configJSON, final Gson gson,
+ final Class clazz) {
+ return Objects.requireNonNull(gson.fromJson(configJSON, clazz));
+ }
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/Availability.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/Availability.java
new file mode 100644
index 0000000000000..ba4be867337e0
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/Availability.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.config.dto;
+
+/**
+ * MQTT topic subscribed to receive availability (online/offline) updates. Must not be used together with
+ * availability_topic
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+public class Availability {
+ protected String payload_available = "online";
+ protected String payload_not_available = "offline";
+ protected String topic;
+
+ public String getPayload_available() {
+ return payload_available;
+ }
+
+ public String getPayload_not_available() {
+ return payload_not_available;
+ }
+
+ public String getTopic() {
+ return topic;
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/AvailabilityMode.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/AvailabilityMode.java
new file mode 100644
index 0000000000000..7389994dd52da
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/AvailabilityMode.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.config.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * controls the conditions needed to set the entity to available
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+public enum AvailabilityMode {
+ /**
+ * payload_available must be received on all configured availability topics before the entity is marked as online
+ */
+ @SerializedName("all")
+ ALL,
+
+ /**
+ * payload_available must be received on at least one configured availability topic before the entity is marked as
+ * online
+ */
+ @SerializedName("any")
+ ANY,
+
+ /**
+ * the last payload_available or payload_not_available received on any configured availability topic controls the
+ * availability
+ */
+ @SerializedName("latest")
+ LATEST
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/Connection.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/Connection.java
new file mode 100644
index 0000000000000..fe2455286ab6b
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/Connection.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.config.dto;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mqtt.homeassistant.internal.config.ConnectionDeserializer;
+
+import com.google.gson.annotations.JsonAdapter;
+
+/**
+ * Connection configuration
+ *
+ * @author Jochen Klein - Initial contribution
+ */
+@JsonAdapter(ConnectionDeserializer.class)
+public class Connection {
+ protected @Nullable String type;
+ protected @Nullable String identifier;
+
+ public Connection() {
+ }
+
+ public Connection(@Nullable String type, @Nullable String identifier) {
+ this.type = type;
+ this.identifier = identifier;
+ }
+
+ @Nullable
+ public String getType() {
+ return type;
+ }
+
+ @Nullable
+ public String getIdentifier() {
+ return identifier;
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/Device.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/Device.java
new file mode 100644
index 0000000000000..84b521508b828
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/dto/Device.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.config.dto;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mqtt.homeassistant.internal.config.ListOrStringDeserializer;
+
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Device configuration
+ *
+ * @author Jochen Klein - Initial contribution
+ */
+public class Device {
+ @JsonAdapter(ListOrStringDeserializer.class)
+ protected @Nullable List identifiers;
+ protected @Nullable List connections;
+ protected @Nullable String manufacturer;
+ protected @Nullable String model;
+ protected @Nullable String name;
+
+ @SerializedName("sw_version")
+ protected @Nullable String swVersion;
+
+ public @Nullable String getId() {
+ List identifiers = this.identifiers;
+ return identifiers == null ? null : String.join("_", identifiers);
+ }
+
+ @Nullable
+ public List getConnections() {
+ return connections;
+ }
+
+ @Nullable
+ public String getManufacturer() {
+ return manufacturer;
+ }
+
+ @Nullable
+ public String getModel() {
+ return model;
+ }
+
+ @Nullable
+ public String getName() {
+ return name;
+ }
+
+ @Nullable
+ public String getSwVersion() {
+ return swVersion;
+ }
+
+ @Nullable
+ public List getIdentifiers() {
+ return identifiers;
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscovery.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscovery.java
index eb8ff8b2dab4b..ed00f64a1e8ee 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscovery.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscovery.java
@@ -33,10 +33,10 @@
import org.openhab.binding.mqtt.discovery.MQTTTopicDiscoveryService;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
-import org.openhab.binding.mqtt.homeassistant.internal.BaseChannelConfiguration;
-import org.openhab.binding.mqtt.homeassistant.internal.ChannelConfigurationTypeAdapterFactory;
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.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
@@ -147,7 +147,7 @@ public void receivedMessage(ThingUID connectionBridge, MqttBrokerConnection conn
}
this.future = scheduler.schedule(this::publishResults, 2, TimeUnit.SECONDS);
- BaseChannelConfiguration config = BaseChannelConfiguration
+ AbstractChannelConfiguration config = AbstractChannelConfiguration
.fromString(new String(payload, StandardCharsets.UTF_8), gson);
// We will of course find multiple of the same unique Thing IDs, for each different component another one.
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java
index a6d9261162670..bc66b1ca9789f 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java
@@ -32,14 +32,14 @@
import org.openhab.binding.mqtt.generic.tools.DelayedBatchProcessing;
import org.openhab.binding.mqtt.generic.utils.FutureCollector;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
-import org.openhab.binding.mqtt.homeassistant.internal.AbstractComponent;
-import org.openhab.binding.mqtt.homeassistant.internal.CChannel;
-import org.openhab.binding.mqtt.homeassistant.internal.CFactory;
-import org.openhab.binding.mqtt.homeassistant.internal.ChannelConfigurationTypeAdapterFactory;
+import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
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.component.AbstractComponent;
+import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory;
+import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
@@ -153,12 +153,12 @@ public void initialize() {
if (channelConfigurationJSON == null) {
logger.warn("Provided channel does not have a 'config' configuration key!");
} else {
- component = CFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this, scheduler,
- gson, transformationServiceProvider);
+ component = ComponentFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this,
+ scheduler, gson, transformationServiceProvider);
}
if (component != null) {
- haComponents.put(component.uid().getId(), component);
+ haComponents.put(component.getGroupUID().getId(), component);
component.addChannelTypes(channelTypeProvider);
} else {
logger.warn("Could not restore component {}", thing);
@@ -235,7 +235,7 @@ protected void stop() {
if (component == null) {
return null;
}
- CChannel componentChannel = component.channel(channelUID.getIdWithoutGroup());
+ ComponentChannel componentChannel = component.getChannel(channelUID.getIdWithoutGroup());
if (componentChannel == null) {
return null;
}
@@ -264,7 +264,7 @@ public void accept(List> discoveredComponentsList) {
synchronized (haComponents) { // sync whenever discoverComponents is started
for (AbstractComponent> discovered : discoveredComponentsList) {
- AbstractComponent> known = haComponents.get(discovered.uid().getId());
+ AbstractComponent> known = haComponents.get(discovered.getGroupUID().getId());
// Is component already known?
if (known != null) {
if (discovered.getConfigHash() != known.getConfigHash()) {
@@ -280,15 +280,15 @@ public void accept(List> discoveredComponentsList) {
// Add channel and group types to the types registry
discovered.addChannelTypes(channelTypeProvider);
// Add component to the component map
- haComponents.put(discovered.uid().getId(), discovered);
+ haComponents.put(discovered.getGroupUID().getId(), discovered);
// Start component / Subscribe to channel topics
discovered.start(connection, scheduler, 0).exceptionally(e -> {
- logger.warn("Failed to start component {}", discovered.uid(), e);
+ logger.warn("Failed to start component {}", discovered.getGroupUID(), e);
return null;
});
- Collection channels = discovered.channelTypes().values().stream().map(CChannel::getChannel)
- .collect(Collectors.toList());
+ Collection channels = discovered.getChannelMap().values().stream()
+ .map(ComponentChannel::getChannel).collect(Collectors.toList());
ThingHelper.addChannelsToThing(thing, channels);
}
}
@@ -314,7 +314,7 @@ private void updateThingType() {
synchronized (haComponents) { // sync whenever discoverComponents is started
groupDefs = haComponents.values().stream().map(AbstractComponent::getGroupDefinition)
.collect(Collectors.toList());
- channelDefs = haComponents.values().stream().map(AbstractComponent::type)
+ channelDefs = haComponents.values().stream().map(AbstractComponent::getType)
.map(ChannelGroupType::getChannelDefinitions).flatMap(List::stream)
.collect(Collectors.toList());
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/listener/ExpireUpdateStateListener.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/listener/ExpireUpdateStateListener.java
index 9427ffb0e3f7b..15a9ef8f20a7d 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/listener/ExpireUpdateStateListener.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/listener/ExpireUpdateStateListener.java
@@ -38,7 +38,7 @@ public class ExpireUpdateStateListener extends ChannelStateUpdateListenerProxy {
private final AvailabilityTracker tracker;
private final ScheduledExecutorService scheduler;
- private AtomicReference<@Nullable ScheduledFuture>> expire = new AtomicReference<>();
+ private final AtomicReference<@Nullable ScheduledFuture>> expire = new AtomicReference<>();
public ExpireUpdateStateListener(ChannelStateUpdateListener original, int expireAfter, Value value,
AvailabilityTracker tracker, ScheduledExecutorService scheduler) {
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/listener/OffDelayUpdateStateListener.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/listener/OffDelayUpdateStateListener.java
index 16b83ae026df7..9b117705bdf6c 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/listener/OffDelayUpdateStateListener.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/listener/OffDelayUpdateStateListener.java
@@ -37,7 +37,7 @@ public class OffDelayUpdateStateListener extends ChannelStateUpdateListenerProxy
private final Value value;
private final ScheduledExecutorService scheduler;
- private AtomicReference<@Nullable ScheduledFuture>> delay = new AtomicReference<>();
+ private final AtomicReference<@Nullable ScheduledFuture>> delay = new AtomicReference<>();
public OffDelayUpdateStateListener(ChannelStateUpdateListener original, int offDelay, Value value,
ScheduledExecutorService scheduler) {
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java
new file mode 100644
index 0000000000000..49879ac8c6a2c
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java
@@ -0,0 +1,189 @@
+/**
+ * Copyright (c) 2010-2021 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.internal;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+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.MqttChannelTypeProvider;
+import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
+import org.openhab.binding.mqtt.handler.BrokerHandler;
+import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
+import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
+import org.openhab.core.test.java.JavaTest;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.builder.BridgeBuilder;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.thing.type.ThingTypeBuilder;
+import org.openhab.core.thing.type.ThingTypeRegistry;
+import org.openhab.transform.jinja.internal.JinjaTransformationService;
+import org.openhab.transform.jinja.internal.profiles.JinjaTransformationProfile;
+
+/**
+ * Abstract class for HomeAssistant unit tests.
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+@SuppressWarnings({ "ConstantConditions" })
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.WARN)
+@NonNullByDefault
+public abstract class AbstractHomeAssistantTests extends JavaTest {
+ public static final String BINDING_ID = "mqtt";
+
+ public static final String BRIDGE_TYPE_ID = "broker";
+ public static final String BRIDGE_TYPE_LABEL = "MQTT Broker";
+ public static final ThingTypeUID BRIDGE_TYPE_UID = new ThingTypeUID(BINDING_ID, BRIDGE_TYPE_ID);
+ public static final String BRIDGE_ID = UUID.randomUUID().toString();
+ public static final ThingUID BRIDGE_UID = new ThingUID(BRIDGE_TYPE_UID, BRIDGE_ID);
+
+ public static final String HA_TYPE_ID = "homeassistant";
+ public static final String HA_TYPE_LABEL = "Homeassistant";
+ public static final ThingTypeUID HA_TYPE_UID = new ThingTypeUID(BINDING_ID, HA_TYPE_ID);
+ public static final String HA_ID = UUID.randomUUID().toString();
+ public static final ThingUID HA_UID = new ThingUID(HA_TYPE_UID, HA_ID);
+
+ protected @Mock @NonNullByDefault({}) MqttBrokerConnection bridgeConnection;
+ protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry;
+ protected @Mock @NonNullByDefault({}) TransformationServiceProvider transformationServiceProvider;
+
+ @SuppressWarnings("NotNullFieldNotInitialized")
+ protected @NonNullByDefault({}) MqttChannelTypeProvider channelTypeProvider;
+
+ protected final Bridge bridgeThing = BridgeBuilder.create(BRIDGE_TYPE_UID, BRIDGE_UID).build();
+ protected final BrokerHandler bridgeHandler = spy(new BrokerHandler(bridgeThing));
+ protected final Thing haThing = ThingBuilder.create(HA_TYPE_UID, HA_UID).withBridge(BRIDGE_UID).build();
+ protected final Map> subscriptions = new HashMap<>();
+
+ private final JinjaTransformationService jinjaTransformationService = new JinjaTransformationService();
+
+ @BeforeEach
+ public void beforeEachAbstractHomeAssistantTests() {
+ when(thingTypeRegistry.getThingType(BRIDGE_TYPE_UID))
+ .thenReturn(ThingTypeBuilder.instance(BRIDGE_TYPE_UID, BRIDGE_TYPE_LABEL).build());
+ when(thingTypeRegistry.getThingType(HA_TYPE_UID))
+ .thenReturn(ThingTypeBuilder.instance(HA_TYPE_UID, HA_TYPE_LABEL).build());
+ when(transformationServiceProvider
+ .getTransformationService(JinjaTransformationProfile.PROFILE_TYPE_UID.getId()))
+ .thenReturn(jinjaTransformationService);
+
+ channelTypeProvider = spy(new MqttChannelTypeProvider(thingTypeRegistry));
+
+ setupConnection();
+
+ // Return the mocked connection object if the bridge handler is asked for it
+ when(bridgeHandler.getConnectionAsync()).thenReturn(CompletableFuture.completedFuture(bridgeConnection));
+
+ bridgeThing.setStatusInfo(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.ONLINE.NONE, ""));
+ bridgeThing.setHandler(bridgeHandler);
+
+ haThing.setStatusInfo(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.ONLINE.NONE, ""));
+ }
+
+ protected void setupConnection() {
+ doAnswer(invocation -> {
+ final var topic = (String) invocation.getArgument(0);
+ final var subscriber = (MqttMessageSubscriber) invocation.getArgument(1);
+ final var topicSubscriptions = subscriptions.getOrDefault(topic, new HashSet<>());
+
+ topicSubscriptions.add(subscriber);
+ subscriptions.put(topic, topicSubscriptions);
+ return CompletableFuture.completedFuture(true);
+ }).when(bridgeConnection).subscribe(any(), any());
+
+ doAnswer(invocation -> {
+ final var topic = (String) invocation.getArgument(0);
+ final var subscriber = (MqttMessageSubscriber) invocation.getArgument(1);
+ final var topicSubscriptions = subscriptions.get(topic);
+
+ if (topicSubscriptions != null) {
+ topicSubscriptions.remove(subscriber);
+ }
+ return CompletableFuture.completedFuture(true);
+ }).when(bridgeConnection).unsubscribe(any(), any());
+
+ doAnswer(invocation -> {
+ subscriptions.clear();
+ return CompletableFuture.completedFuture(true);
+ }).when(bridgeConnection).unsubscribeAll();
+
+ doReturn(CompletableFuture.completedFuture(true)).when(bridgeConnection).publish(any(), any(), anyInt(),
+ anyBoolean());
+ }
+
+ /**
+ * @param relativePath path from src/test/java/org/openhab/binding/mqtt/homeassistant/internal
+ * @return path
+ */
+ protected Path getResourcePath(String relativePath) {
+ try {
+ return Paths.get(AbstractHomeAssistantTests.class.getResource(relativePath).toURI());
+ } catch (URISyntaxException e) {
+ Assertions.fail(e);
+ }
+ throw new IllegalArgumentException();
+ }
+
+ protected String getResourceAsString(String relativePath) {
+ try {
+ return Files.readString(getResourcePath(relativePath));
+ } catch (IOException e) {
+ Assertions.fail(e);
+ }
+ throw new IllegalArgumentException();
+ }
+
+ protected byte[] getResourceAsByteArray(String relativePath) {
+ try {
+ return Files.readAllBytes(getResourcePath(relativePath));
+ } catch (IOException e) {
+ Assertions.fail(e);
+ }
+ throw new IllegalArgumentException();
+ }
+
+ protected static String configTopicToMqtt(String configTopic) {
+ return HandlerConfiguration.DEFAULT_BASETOPIC + "/" + configTopic + "/config";
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HAConfigurationTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HAConfigurationTests.java
deleted file mode 100644
index e25ccf92a49ef..0000000000000
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HAConfigurationTests.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/**
- * Copyright (c) 2010-2021 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.internal;
-
-import static org.hamcrest.CoreMatchers.*;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.Arrays;
-import java.util.List;
-
-import org.eclipse.jdt.annotation.NonNull;
-import org.junit.jupiter.api.Test;
-import org.openhab.binding.mqtt.homeassistant.internal.BaseChannelConfiguration.Connection;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-
-/**
- * @author Jochen Klein - Initial contribution
- */
-public class HAConfigurationTests {
-
- private Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory())
- .create();
-
- private static String readTestJson(final String name) {
- StringBuilder result = new StringBuilder();
-
- try (BufferedReader in = new BufferedReader(
- new InputStreamReader(HAConfigurationTests.class.getResourceAsStream(name), "UTF-8"))) {
- String line;
-
- while ((line = in.readLine()) != null) {
- result.append(line).append('\n');
- }
- return result.toString();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Test
- public void testAbbreviations() {
- String json = readTestJson("configA.json");
-
- BaseChannelConfiguration config = BaseChannelConfiguration.fromString(json, gson);
-
- assertThat(config.name, is("A"));
- assertThat(config.icon, is("2"));
- assertThat(config.qos, is(1));
- assertThat(config.retain, is(true));
- assertThat(config.value_template, is("B"));
- assertThat(config.unique_id, is("C"));
- assertThat(config.availability_topic, is("D/E"));
- assertThat(config.payload_available, is("F"));
- assertThat(config.payload_not_available, is("G"));
-
- assertThat(config.device, is(notNullValue()));
-
- BaseChannelConfiguration.Device device = config.device;
- if (device != null) {
- assertThat(device.identifiers, contains("H"));
- assertThat(device.connections, is(notNullValue()));
- List<@NonNull Connection> connections = device.connections;
- if (connections != null) {
- assertThat(connections.get(0).type, is("I1"));
- assertThat(connections.get(0).identifier, is("I2"));
- }
- assertThat(device.name, is("J"));
- assertThat(device.model, is("K"));
- assertThat(device.sw_version, is("L"));
- assertThat(device.manufacturer, is("M"));
- }
- }
-
- @Test
- public void testTildeSubstritution() {
- String json = readTestJson("configB.json");
-
- ComponentSwitch.ChannelConfiguration config = BaseChannelConfiguration.fromString(json, gson,
- ComponentSwitch.ChannelConfiguration.class);
-
- assertThat(config.availability_topic, is("D/E"));
- assertThat(config.state_topic, is("O/D/"));
- assertThat(config.command_topic, is("P~Q"));
- assertThat(config.device, is(notNullValue()));
-
- BaseChannelConfiguration.Device device = config.device;
- if (device != null) {
- assertThat(device.identifiers, contains("H"));
- }
- }
-
- @Test
- public void testSampleFanConfig() {
- String json = readTestJson("configFan.json");
-
- ComponentFan.ChannelConfiguration config = BaseChannelConfiguration.fromString(json, gson,
- ComponentFan.ChannelConfiguration.class);
- assertThat(config.name, is("Bedroom Fan"));
- }
-
- @Test
- public void testDeviceListConfig() {
- String json = readTestJson("configDeviceList.json");
-
- ComponentFan.ChannelConfiguration config = BaseChannelConfiguration.fromString(json, gson,
- ComponentFan.ChannelConfiguration.class);
- assertThat(config.device, is(notNullValue()));
-
- BaseChannelConfiguration.Device device = config.device;
- if (device != null) {
- assertThat(device.identifiers, is(Arrays.asList("A", "B", "C")));
- }
- }
-
- @Test
- public void testDeviceSingleStringConfig() {
- String json = readTestJson("configDeviceSingleString.json");
-
- ComponentFan.ChannelConfiguration config = BaseChannelConfiguration.fromString(json, gson,
- ComponentFan.ChannelConfiguration.class);
- assertThat(config.device, is(notNullValue()));
-
- BaseChannelConfiguration.Device device = config.device;
- if (device != null) {
- assertThat(device.identifiers, is(Arrays.asList("A")));
- }
- }
-}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java
new file mode 100644
index 0000000000000..f35aa45db174c
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java
@@ -0,0 +1,269 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.component;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.hamcrest.CoreMatchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.mockito.Mock;
+import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
+import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
+import org.openhab.binding.mqtt.generic.values.Value;
+import org.openhab.binding.mqtt.homeassistant.internal.AbstractHomeAssistantTests;
+import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
+import org.openhab.binding.mqtt.homeassistant.internal.HaID;
+import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
+import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.types.State;
+
+/**
+ * Abstract class for components tests.
+ * TODO: need a way to test all channel properties, not only topics.
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+@SuppressWarnings({ "ConstantConditions" })
+public abstract class AbstractComponentTests extends AbstractHomeAssistantTests {
+ private final static int SUBSCRIBE_TIMEOUT = 10000;
+ private final static int ATTRIBUTE_RECEIVE_TIMEOUT = 2000;
+
+ private @Mock ThingHandlerCallback callback;
+ private LatchThingHandler thingHandler;
+
+ @BeforeEach
+ public void setupThingHandler() {
+ final var config = haThing.getConfiguration();
+
+ config.put(HandlerConfiguration.PROPERTY_BASETOPIC, HandlerConfiguration.DEFAULT_BASETOPIC);
+ config.put(HandlerConfiguration.PROPERTY_TOPICS, getConfigTopics());
+
+ when(callback.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
+
+ thingHandler = new LatchThingHandler(haThing, channelTypeProvider, transformationServiceProvider,
+ SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
+ thingHandler.setConnection(bridgeConnection);
+ thingHandler.setCallback(callback);
+ thingHandler = spy(thingHandler);
+
+ thingHandler.initialize();
+ }
+
+ @AfterEach
+ public void disposeThingHandler() {
+ thingHandler.dispose();
+ }
+
+ /**
+ * {@link org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents} will wait a config on specified
+ * topics.
+ * Topics in config must be without prefix and suffix, they can be converted to full with method
+ * {@link #configTopicToMqtt(String)}
+ *
+ * @return config topics
+ */
+ protected abstract Set getConfigTopics();
+
+ /**
+ * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
+ *
+ * @param mqttTopic mqtt topic with configuration
+ * @param json configuration payload in Json
+ * @return discovered component
+ */
+ protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
+ String json) {
+ return discoverComponent(mqttTopic, json.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
+ *
+ * @param mqttTopic mqtt topic with configuration
+ * @param jsonPayload configuration payload in Json
+ * @return discovered component
+ */
+ protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
+ byte[] jsonPayload) {
+ var latch = thingHandler.createWaitForComponentDiscoveredLatch(1);
+ assertThat(publishMessage(mqttTopic, jsonPayload), is(true));
+ try {
+ assert latch.await(1, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ assertThat(e.getMessage(), false);
+ }
+ var component = thingHandler.getDiscoveredComponent();
+ assertThat(component, CoreMatchers.notNullValue());
+ return component;
+ }
+
+ /**
+ * Assert channel topics, label and value class
+ *
+ * @param component component
+ * @param channelId channel
+ * @param stateTopic state topic or empty string
+ * @param commandTopic command topic or empty string
+ * @param label label
+ * @param valueClass value class
+ */
+ protected static void assertChannel(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
+ String channelId, String stateTopic, String commandTopic, String label, Class extends Value> valueClass) {
+ var stateChannel = component.getChannel(channelId);
+ assertChannel(stateChannel, stateTopic, commandTopic, label, valueClass);
+ }
+
+ /**
+ * Assert channel topics, label and value class
+ *
+ * @param stateChannel channel
+ * @param stateTopic state topic or empty string
+ * @param commandTopic command topic or empty string
+ * @param label label
+ * @param valueClass value class
+ */
+ protected static void assertChannel(ComponentChannel stateChannel, String stateTopic, String commandTopic,
+ String label, Class extends Value> valueClass) {
+ assertThat(stateChannel.getChannel().getLabel(), is(label));
+ assertThat(stateChannel.getState().getStateTopic(), is(stateTopic));
+ assertThat(stateChannel.getState().getCommandTopic(), is(commandTopic));
+ assertThat(stateChannel.getState().getCache(), is(instanceOf(valueClass)));
+ }
+
+ /**
+ * Assert channel state
+ *
+ * @param component component
+ * @param channelId channel
+ * @param state expected state
+ */
+ protected static void assertState(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
+ String channelId, State state) {
+ assertThat(component.getChannel(channelId).getState().getCache().getChannelState(), is(state));
+ }
+
+ /**
+ * Assert that given payload was published exact-once on given topic.
+ *
+ * @param mqttTopic Mqtt topic
+ * @param payload payload
+ */
+ protected void assertPublished(String mqttTopic, String payload) {
+ verify(bridgeConnection).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(),
+ anyBoolean());
+ }
+
+ /**
+ * Assert that given payload was published N times on given topic.
+ *
+ * @param mqttTopic Mqtt topic
+ * @param payload payload
+ * @param t payload must be published N times on given topic
+ */
+ protected void assertPublished(String mqttTopic, String payload, int t) {
+ verify(bridgeConnection, times(t)).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)),
+ anyInt(), anyBoolean());
+ }
+
+ /**
+ * Assert that given payload was not published on given topic.
+ *
+ * @param mqttTopic Mqtt topic
+ * @param payload payload
+ */
+ protected void assertNotPublished(String mqttTopic, String payload) {
+ verify(bridgeConnection, never()).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(),
+ anyBoolean());
+ }
+
+ /**
+ * Publish payload to all subscribers on specified topic.
+ *
+ * @param mqttTopic Mqtt topic
+ * @param payload payload
+ * @return true when at least one subscriber found
+ */
+ protected boolean publishMessage(String mqttTopic, String payload) {
+ return publishMessage(mqttTopic, payload.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Publish payload to all subscribers on specified topic.
+ *
+ * @param mqttTopic Mqtt topic
+ * @param payload payload
+ * @return true when at least one subscriber found
+ */
+ protected boolean publishMessage(String mqttTopic, byte[] payload) {
+ final var topicSubscribers = subscriptions.get(mqttTopic);
+
+ if (topicSubscribers != null && !topicSubscribers.isEmpty()) {
+ topicSubscribers.forEach(mqttMessageSubscriber -> mqttMessageSubscriber.processMessage(mqttTopic, payload));
+ return true;
+ }
+ return false;
+ }
+
+ @NonNullByDefault
+ protected static class LatchThingHandler extends HomeAssistantThingHandler {
+ private @Nullable CountDownLatch latch;
+ private @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoveredComponent;
+
+ public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
+ TransformationServiceProvider transformationServiceProvider, int subscribeTimeout,
+ int attributeReceiveTimeout) {
+ super(thing, channelTypeProvider, transformationServiceProvider, subscribeTimeout, attributeReceiveTimeout);
+ }
+
+ public void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<@NonNull ?> component) {
+ accept(List.of(component));
+ discoveredComponent = component;
+ if (latch != null) {
+ latch.countDown();
+ }
+ }
+
+ public CountDownLatch createWaitForComponentDiscoveredLatch(int count) {
+ final var newLatch = new CountDownLatch(count);
+ latch = newLatch;
+ return newLatch;
+ }
+
+ public @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> getDiscoveredComponent() {
+ return discoveredComponent;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanelTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanelTests.java
new file mode 100644
index 0000000000000..6aa60e9705e64
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanelTests.java
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.generic.values.TextValue;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * Tests for {@link AlarmControlPanel}
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+@SuppressWarnings("ConstantConditions")
+public class AlarmControlPanelTests extends AbstractComponentTests {
+ public static final String CONFIG_TOPIC = "alarm_control_panel/0x0000000000000000_alarm_control_panel_zigbee2mqtt";
+
+ @Test
+ public void testAlarmControlPanel() {
+ // @formatter:off
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ "{ " +
+ " \"availability\": [ " +
+ " { " +
+ " \"topic\": \"zigbee2mqtt/bridge/state\" " +
+ " } " +
+ " ], " +
+ " \"code\": \"12345\", " +
+ " \"command_topic\": \"zigbee2mqtt/alarm/set/state\", " +
+ " \"device\": { " +
+ " \"identifiers\": [ " +
+ " \"zigbee2mqtt_0x0000000000000000\" " +
+ " ], " +
+ " \"manufacturer\": \"BestAlarmEver\", " +
+ " \"model\": \"Heavy duty super duper alarm\", " +
+ " \"name\": \"Alarm\", " +
+ " \"sw_version\": \"Zigbee2MQTT 1.18.2\" " +
+ " }, " +
+ " \"name\": \"alarm\", " +
+ " \"payload_arm_away\": \"ARM_AWAY_\", " +
+ " \"payload_arm_home\": \"ARM_HOME_\", " +
+ " \"payload_arm_night\": \"ARM_NIGHT_\", " +
+ " \"payload_arm_custom_bypass\": \"ARM_CUSTOM_BYPASS_\", " +
+ " \"payload_disarm\": \"DISARM_\", " +
+ " \"state_topic\": \"zigbee2mqtt/alarm/state\" " +
+ "} ");
+ // @formatter:on
+
+ assertThat(component.channels.size(), is(4));
+ assertThat(component.getName(), is("alarm"));
+
+ assertChannel(component, AlarmControlPanel.stateChannelID, "zigbee2mqtt/alarm/state", "", "alarm",
+ TextValue.class);
+ assertChannel(component, AlarmControlPanel.switchDisarmChannelID, "", "zigbee2mqtt/alarm/set/state", "alarm",
+ TextValue.class);
+ assertChannel(component, AlarmControlPanel.switchArmAwayChannelID, "", "zigbee2mqtt/alarm/set/state", "alarm",
+ TextValue.class);
+ assertChannel(component, AlarmControlPanel.switchArmHomeChannelID, "", "zigbee2mqtt/alarm/set/state", "alarm",
+ TextValue.class);
+
+ publishMessage("zigbee2mqtt/alarm/state", "armed_home");
+ assertState(component, AlarmControlPanel.stateChannelID, new StringType("armed_home"));
+ publishMessage("zigbee2mqtt/alarm/state", "armed_away");
+ assertState(component, AlarmControlPanel.stateChannelID, new StringType("armed_away"));
+
+ component.getChannel(AlarmControlPanel.switchDisarmChannelID).getState()
+ .publishValue(new StringType("DISARM_"));
+ assertPublished("zigbee2mqtt/alarm/set/state", "DISARM_");
+ component.getChannel(AlarmControlPanel.switchArmAwayChannelID).getState()
+ .publishValue(new StringType("ARM_AWAY_"));
+ assertPublished("zigbee2mqtt/alarm/set/state", "ARM_AWAY_");
+ component.getChannel(AlarmControlPanel.switchArmHomeChannelID).getState()
+ .publishValue(new StringType("ARM_HOME_"));
+ assertPublished("zigbee2mqtt/alarm/set/state", "ARM_HOME_");
+ }
+
+ protected Set getConfigTopics() {
+ return Set.of(CONFIG_TOPIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/BinarySensorTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/BinarySensorTests.java
new file mode 100644
index 0000000000000..8768853779e63
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/BinarySensorTests.java
@@ -0,0 +1,154 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * Tests for {@link BinarySensor}
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+public class BinarySensorTests extends AbstractComponentTests {
+ public static final String CONFIG_TOPIC = "binary_sensor/0x0000000000000000_binary_sensor_zigbee2mqtt";
+
+ @Test
+ public void test() throws InterruptedException {
+ // @formatter:off
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ "{ " +
+ " \"availability\": [ " +
+ " { " +
+ " \"topic\": \"zigbee2mqtt/bridge/state\" " +
+ " } " +
+ " ], " +
+ " \"device\": { " +
+ " \"identifiers\": [ " +
+ " \"zigbee2mqtt_0x0000000000000000\" " +
+ " ], " +
+ " \"manufacturer\": \"Sensors inc\", " +
+ " \"model\": \"On Off Sensor\", " +
+ " \"name\": \"OnOffSensor\", " +
+ " \"sw_version\": \"Zigbee2MQTT 1.18.2\" " +
+ " }, " +
+ " \"name\": \"onoffsensor\", " +
+ " \"force_update\": \"true\", " +
+ " \"payload_off\": \"OFF_\", " +
+ " \"payload_on\": \"ON_\", " +
+ " \"state_topic\": \"zigbee2mqtt/sensor/state\", " +
+ " \"unique_id\": \"sn1\", " +
+ " \"value_template\": \"{{ value_json.state }}\" " +
+ "}");
+ // @formatter:on
+
+ assertThat(component.channels.size(), is(1));
+ assertThat(component.getName(), is("onoffsensor"));
+ assertThat(component.getGroupUID().getId(), is("sn1"));
+
+ assertChannel(component, BinarySensor.sensorChannelID, "zigbee2mqtt/sensor/state", "", "value",
+ OnOffValue.class);
+
+ publishMessage("zigbee2mqtt/sensor/state", "{ \"state\": \"ON_\" }");
+ assertState(component, BinarySensor.sensorChannelID, OnOffType.ON);
+ publishMessage("zigbee2mqtt/sensor/state", "{ \"state\": \"ON_\" }");
+ assertState(component, BinarySensor.sensorChannelID, OnOffType.ON);
+ publishMessage("zigbee2mqtt/sensor/state", "{ \"state\": \"OFF_\" }");
+ assertState(component, BinarySensor.sensorChannelID, OnOffType.OFF);
+ publishMessage("zigbee2mqtt/sensor/state", "{ \"state\": \"ON_\" }");
+ assertState(component, BinarySensor.sensorChannelID, OnOffType.ON);
+ }
+
+ @Test
+ public void offDelayTest() {
+ // @formatter:off
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ "{ " +
+ " \"availability\": [ " +
+ " { " +
+ " \"topic\": \"zigbee2mqtt/bridge/state\" " +
+ " } " +
+ " ], " +
+ " \"device\": { " +
+ " \"identifiers\": [ " +
+ " \"zigbee2mqtt_0x0000000000000000\" " +
+ " ], " +
+ " \"manufacturer\": \"Sensors inc\", " +
+ " \"model\": \"On Off Sensor\", " +
+ " \"name\": \"OnOffSensor\", " +
+ " \"sw_version\": \"Zigbee2MQTT 1.18.2\" " +
+ " }, " +
+ " \"name\": \"onoffsensor\", " +
+ " \"force_update\": \"true\", " +
+ " \"off_delay\": \"1\", " +
+ " \"payload_off\": \"OFF_\", " +
+ " \"payload_on\": \"ON_\", " +
+ " \"state_topic\": \"zigbee2mqtt/sensor/state\", " +
+ " \"unique_id\": \"sn1\", " +
+ " \"value_template\": \"{{ value_json.state }}\" " +
+ "}");
+ // @formatter:on
+
+ publishMessage("zigbee2mqtt/sensor/state", "{ \"state\": \"ON_\" }");
+ assertState(component, BinarySensor.sensorChannelID, OnOffType.ON);
+
+ waitForAssert(() -> assertState(component, BinarySensor.sensorChannelID, OnOffType.OFF), 10000, 200);
+ }
+
+ @Test
+ public void expireAfterTest() {
+ // @formatter:off
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ "{ " +
+ " \"availability\": [ " +
+ " { " +
+ " \"topic\": \"zigbee2mqtt/bridge/state\" " +
+ " } " +
+ " ], " +
+ " \"device\": { " +
+ " \"identifiers\": [ " +
+ " \"zigbee2mqtt_0x0000000000000000\" " +
+ " ], " +
+ " \"manufacturer\": \"Sensors inc\", " +
+ " \"model\": \"On Off Sensor\", " +
+ " \"name\": \"OnOffSensor\", " +
+ " \"sw_version\": \"Zigbee2MQTT 1.18.2\" " +
+ " }, " +
+ " \"name\": \"onoffsensor\", " +
+ " \"expire_after\": \"1\", " +
+ " \"force_update\": \"true\", " +
+ " \"payload_off\": \"OFF_\", " +
+ " \"payload_on\": \"ON_\", " +
+ " \"state_topic\": \"zigbee2mqtt/sensor/state\", " +
+ " \"unique_id\": \"sn1\", " +
+ " \"value_template\": \"{{ value_json.state }}\" " +
+ "}");
+ // @formatter:on
+
+ publishMessage("zigbee2mqtt/sensor/state", "{ \"state\": \"OFF_\" }");
+ assertState(component, BinarySensor.sensorChannelID, OnOffType.OFF);
+
+ waitForAssert(() -> assertState(component, BinarySensor.sensorChannelID, UnDefType.UNDEF), 10000, 200);
+ }
+
+ protected Set getConfigTopics() {
+ return Set.of(CONFIG_TOPIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/CameraTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/CameraTests.java
new file mode 100644
index 0000000000000..7b7f794dad862
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/CameraTests.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.generic.values.ImageValue;
+import org.openhab.core.library.types.RawType;
+
+/**
+ * Tests for {@link Camera}
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+public class CameraTests extends AbstractComponentTests {
+ public static final String CONFIG_TOPIC = "camera/0x0000000000000000_camera_zigbee2mqtt";
+
+ @Test
+ public void test() throws InterruptedException {
+ // @formatter:off
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ "{ " +
+ " \"availability\": [ " +
+ " { " +
+ " \"topic\": \"zigbee2mqtt/bridge/state\" " +
+ " } " +
+ " ], " +
+ " \"device\": { " +
+ " \"identifiers\": [ " +
+ " \"zigbee2mqtt_0x0000000000000000\" " +
+ " ], " +
+ " \"manufacturer\": \"Cameras inc\", " +
+ " \"model\": \"Camera\", " +
+ " \"name\": \"camera\", " +
+ " \"sw_version\": \"Zigbee2MQTT 1.18.2\" " +
+ " }, " +
+ " \"name\": \"cam1\", " +
+ " \"topic\": \"zigbee2mqtt/cam1/state\"" +
+ "}");
+ // @formatter:on
+
+ assertThat(component.channels.size(), is(1));
+ assertThat(component.getName(), is("cam1"));
+
+ assertChannel(component, Camera.cameraChannelID, "zigbee2mqtt/cam1/state", "", "cam1", ImageValue.class);
+
+ var imageBytes = getResourceAsByteArray("component/image.png");
+ publishMessage("zigbee2mqtt/cam1/state", imageBytes);
+ assertState(component, Camera.cameraChannelID, new RawType(imageBytes, "image/png"));
+ }
+
+ protected Set getConfigTopics() {
+ return Set.of(CONFIG_TOPIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/ClimateTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/ClimateTests.java
new file mode 100644
index 0000000000000..3ceadb3dff0b7
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/ClimateTests.java
@@ -0,0 +1,297 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.generic.values.NumberValue;
+import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.binding.mqtt.generic.values.TextValue;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * Tests for {@link Climate}
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+@SuppressWarnings("ConstantConditions")
+public class ClimateTests extends AbstractComponentTests {
+ public static final String CONFIG_TOPIC = "climate/0x847127fffe11dd6a_climate_zigbee2mqtt";
+
+ @Test
+ public void testTS0601Climate() {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), "{"
+ + " \"action_template\": \"{% set values = {'idle':'off','heat':'heating','cool':'cooling','fan only':'fan'} %}{{ values[value_json.running_state] }}\","
+ + " \"action_topic\": \"zigbee2mqtt/th1\", \"availability\": [ {"
+ + " \"topic\": \"zigbee2mqtt/bridge/state\" } ],"
+ + " \"away_mode_command_topic\": \"zigbee2mqtt/th1/set/away_mode\","
+ + " \"away_mode_state_template\": \"{{ value_json.away_mode }}\","
+ + " \"away_mode_state_topic\": \"zigbee2mqtt/th1\","
+ + " \"current_temperature_template\": \"{{ value_json.local_temperature }}\","
+ + " \"current_temperature_topic\": \"zigbee2mqtt/th1\", \"device\": {"
+ + " \"identifiers\": [ \"zigbee2mqtt_0x847127fffe11dd6a\" ], \"manufacturer\": \"TuYa\","
+ + " \"model\": \"Radiator valve with thermostat (TS0601_thermostat)\","
+ + " \"name\": \"th1\", \"sw_version\": \"Zigbee2MQTT 1.18.2\" },"
+ + " \"hold_command_topic\": \"zigbee2mqtt/th1/set/preset\", \"hold_modes\": ["
+ + " \"schedule\", \"manual\", \"boost\", \"complex\", \"comfort\", \"eco\" ],"
+ + " \"hold_state_template\": \"{{ value_json.preset }}\","
+ + " \"hold_state_topic\": \"zigbee2mqtt/th1\","
+ + " \"json_attributes_topic\": \"zigbee2mqtt/th1\", \"max_temp\": \"35\","
+ + " \"min_temp\": \"5\", \"mode_command_topic\": \"zigbee2mqtt/th1/set/system_mode\","
+ + " \"mode_state_template\": \"{{ value_json.system_mode }}\","
+ + " \"mode_state_topic\": \"zigbee2mqtt/th1\", \"modes\": [ \"heat\","
+ + " \"auto\", \"off\" ], \"name\": \"th1\", \"temp_step\": 0.5,"
+ + " \"temperature_command_topic\": \"zigbee2mqtt/th1/set/current_heating_setpoint\","
+ + " \"temperature_state_template\": \"{{ value_json.current_heating_setpoint }}\","
+ + " \"temperature_state_topic\": \"zigbee2mqtt/th1\", \"temperature_unit\": \"C\","
+ + " \"unique_id\": \"0x847127fffe11dd6a_climate_zigbee2mqtt\"}");
+
+ assertThat(component.channels.size(), is(6));
+ assertThat(component.getName(), is("th1"));
+
+ assertChannel(component, Climate.ACTION_CH_ID, "zigbee2mqtt/th1", "", "th1", TextValue.class);
+ assertChannel(component, Climate.AWAY_MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/away_mode", "th1",
+ OnOffValue.class);
+ assertChannel(component, Climate.CURRENT_TEMPERATURE_CH_ID, "zigbee2mqtt/th1", "", "th1", NumberValue.class);
+ assertChannel(component, Climate.HOLD_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/preset", "th1",
+ TextValue.class);
+ assertChannel(component, Climate.MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/system_mode", "th1",
+ TextValue.class);
+ assertChannel(component, Climate.TEMPERATURE_CH_ID, "zigbee2mqtt/th1",
+ "zigbee2mqtt/th1/set/current_heating_setpoint", "th1", NumberValue.class);
+
+ publishMessage("zigbee2mqtt/th1",
+ "{\"running_state\": \"idle\", \"away_mode\": \"ON\", "
+ + "\"local_temperature\": \"22.2\", \"preset\": \"schedule\", \"system_mode\": \"heat\", "
+ + "\"current_heating_setpoint\": \"24\"}");
+ assertState(component, Climate.ACTION_CH_ID, new StringType("off"));
+ assertState(component, Climate.AWAY_MODE_CH_ID, OnOffType.ON);
+ assertState(component, Climate.CURRENT_TEMPERATURE_CH_ID, new DecimalType(22.2));
+ assertState(component, Climate.HOLD_CH_ID, new StringType("schedule"));
+ assertState(component, Climate.MODE_CH_ID, new StringType("heat"));
+ assertState(component, Climate.TEMPERATURE_CH_ID, new DecimalType(24));
+
+ component.getChannel(Climate.AWAY_MODE_CH_ID).getState().publishValue(OnOffType.OFF);
+ assertPublished("zigbee2mqtt/th1/set/away_mode", "OFF");
+ component.getChannel(Climate.HOLD_CH_ID).getState().publishValue(new StringType("eco"));
+ assertPublished("zigbee2mqtt/th1/set/preset", "eco");
+ component.getChannel(Climate.MODE_CH_ID).getState().publishValue(new StringType("auto"));
+ assertPublished("zigbee2mqtt/th1/set/system_mode", "auto");
+ component.getChannel(Climate.TEMPERATURE_CH_ID).getState().publishValue(new DecimalType(25));
+ assertPublished("zigbee2mqtt/th1/set/current_heating_setpoint", "25");
+ }
+
+ @Test
+ public void testTS0601ClimateNotSendIfOff() {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), "{"
+ + " \"action_template\": \"{% set values = {'idle':'off','heat':'heating','cool':'cooling','fan only':'fan'} %}{{ values[value_json.running_state] }}\","
+ + " \"action_topic\": \"zigbee2mqtt/th1\", \"availability\": [ {"
+ + " \"topic\": \"zigbee2mqtt/bridge/state\" } ],"
+ + " \"away_mode_command_topic\": \"zigbee2mqtt/th1/set/away_mode\","
+ + " \"away_mode_state_template\": \"{{ value_json.away_mode }}\","
+ + " \"away_mode_state_topic\": \"zigbee2mqtt/th1\","
+ + " \"current_temperature_template\": \"{{ value_json.local_temperature }}\","
+ + " \"current_temperature_topic\": \"zigbee2mqtt/th1\", \"device\": {"
+ + " \"identifiers\": [ \"zigbee2mqtt_0x847127fffe11dd6a\" ], \"manufacturer\": \"TuYa\","
+ + " \"model\": \"Radiator valve with thermostat (TS0601_thermostat)\","
+ + " \"name\": \"th1\", \"sw_version\": \"Zigbee2MQTT 1.18.2\" },"
+ + " \"hold_command_topic\": \"zigbee2mqtt/th1/set/preset\", \"hold_modes\": ["
+ + " \"schedule\", \"manual\", \"boost\", \"complex\", \"comfort\", \"eco\" ],"
+ + " \"hold_state_template\": \"{{ value_json.preset }}\","
+ + " \"hold_state_topic\": \"zigbee2mqtt/th1\","
+ + " \"json_attributes_topic\": \"zigbee2mqtt/th1\", \"max_temp\": \"35\","
+ + " \"min_temp\": \"5\", \"mode_command_topic\": \"zigbee2mqtt/th1/set/system_mode\","
+ + " \"mode_state_template\": \"{{ value_json.system_mode }}\","
+ + " \"mode_state_topic\": \"zigbee2mqtt/th1\", \"modes\": [ \"heat\","
+ + " \"auto\", \"off\" ], \"name\": \"th1\", \"temp_step\": 0.5,"
+ + " \"temperature_command_topic\": \"zigbee2mqtt/th1/set/current_heating_setpoint\","
+ + " \"temperature_state_template\": \"{{ value_json.current_heating_setpoint }}\","
+ + " \"temperature_state_topic\": \"zigbee2mqtt/th1\", \"temperature_unit\": \"C\","
+ + " \"power_command_topic\": \"zigbee2mqtt/th1/power\","
+ + " \"unique_id\": \"0x847127fffe11dd6a_climate_zigbee2mqtt\", \"send_if_off\": \"false\"}");
+
+ assertThat(component.channels.size(), is(7));
+ assertThat(component.getName(), is("th1"));
+
+ assertChannel(component, Climate.ACTION_CH_ID, "zigbee2mqtt/th1", "", "th1", TextValue.class);
+ assertChannel(component, Climate.AWAY_MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/away_mode", "th1",
+ OnOffValue.class);
+ assertChannel(component, Climate.CURRENT_TEMPERATURE_CH_ID, "zigbee2mqtt/th1", "", "th1", NumberValue.class);
+ assertChannel(component, Climate.HOLD_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/preset", "th1",
+ TextValue.class);
+ assertChannel(component, Climate.MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/system_mode", "th1",
+ TextValue.class);
+ assertChannel(component, Climate.TEMPERATURE_CH_ID, "zigbee2mqtt/th1",
+ "zigbee2mqtt/th1/set/current_heating_setpoint", "th1", NumberValue.class);
+
+ publishMessage("zigbee2mqtt/th1",
+ "{\"running_state\": \"idle\", \"away_mode\": \"ON\", "
+ + "\"local_temperature\": \"22.2\", \"preset\": \"schedule\", \"system_mode\": \"heat\", "
+ + "\"current_heating_setpoint\": \"24\"}");
+ assertState(component, Climate.ACTION_CH_ID, new StringType("off"));
+ assertState(component, Climate.AWAY_MODE_CH_ID, OnOffType.ON);
+ assertState(component, Climate.CURRENT_TEMPERATURE_CH_ID, new DecimalType(22.2));
+ assertState(component, Climate.HOLD_CH_ID, new StringType("schedule"));
+ assertState(component, Climate.MODE_CH_ID, new StringType("heat"));
+ assertState(component, Climate.TEMPERATURE_CH_ID, new DecimalType(24));
+
+ // Climate is in OFF state
+ component.getChannel(Climate.AWAY_MODE_CH_ID).getState().publishValue(OnOffType.OFF);
+ assertNotPublished("zigbee2mqtt/th1/set/away_mode", "OFF");
+ component.getChannel(Climate.HOLD_CH_ID).getState().publishValue(new StringType("eco"));
+ assertNotPublished("zigbee2mqtt/th1/set/preset", "eco");
+ component.getChannel(Climate.MODE_CH_ID).getState().publishValue(new StringType("auto"));
+ assertNotPublished("zigbee2mqtt/th1/set/system_mode", "auto");
+ component.getChannel(Climate.TEMPERATURE_CH_ID).getState().publishValue(new DecimalType(25));
+ assertNotPublished("zigbee2mqtt/th1/set/current_heating_setpoint", "25");
+ component.getChannel(Climate.POWER_CH_ID).getState().publishValue(OnOffType.ON);
+ assertPublished("zigbee2mqtt/th1/power", "ON");
+
+ // Enabled
+ publishMessage("zigbee2mqtt/th1",
+ "{\"running_state\": \"heat\", \"away_mode\": \"ON\", "
+ + "\"local_temperature\": \"22.2\", \"preset\": \"schedule\", \"system_mode\": \"heat\", "
+ + "\"current_heating_setpoint\": \"24\"}");
+
+ // Climate is in ON state
+ component.getChannel(Climate.AWAY_MODE_CH_ID).getState().publishValue(OnOffType.OFF);
+ assertPublished("zigbee2mqtt/th1/set/away_mode", "OFF");
+ component.getChannel(Climate.HOLD_CH_ID).getState().publishValue(new StringType("eco"));
+ assertPublished("zigbee2mqtt/th1/set/preset", "eco");
+ component.getChannel(Climate.MODE_CH_ID).getState().publishValue(new StringType("auto"));
+ assertPublished("zigbee2mqtt/th1/set/system_mode", "auto");
+ component.getChannel(Climate.TEMPERATURE_CH_ID).getState().publishValue(new DecimalType(25));
+ assertPublished("zigbee2mqtt/th1/set/current_heating_setpoint", "25");
+ }
+
+ @Test
+ public void testClimate() {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ "{\"action_template\": \"{{ value_json.action }}\", \"action_topic\": \"zigbee2mqtt/th1\","
+ + " \"aux_command_topic\": \"zigbee2mqtt/th1/aux\","
+ + " \"aux_state_template\": \"{{ value_json.aux }}\", \"aux_state_topic\": \"zigbee2mqtt/th1\","
+ + " \"away_mode_command_topic\": \"zigbee2mqtt/th1/away_mode\","
+ + " \"away_mode_state_template\": \"{{ value_json.away_mode }}\","
+ + " \"away_mode_state_topic\": \"zigbee2mqtt/th1\","
+ + " \"current_temperature_template\": \"{{ value_json.current_temperature }}\","
+ + " \"current_temperature_topic\": \"zigbee2mqtt/th1\","
+ + " \"fan_mode_command_template\": \"fan_mode={{ value }}\","
+ + " \"fan_mode_command_topic\": \"zigbee2mqtt/th1/fan_mode\","
+ + " \"fan_mode_state_template\": \"{{ value_json.fan_mode }}\","
+ + " \"fan_mode_state_topic\": \"zigbee2mqtt/th1\", \"fan_modes\": [ \"p1\","
+ + " \"p2\" ], \"hold_command_template\": \"hold={{ value }}\","
+ + " \"hold_command_topic\": \"zigbee2mqtt/th1/hold\","
+ + " \"hold_state_template\": \"{{ value_json.hold }}\","
+ + " \"hold_state_topic\": \"zigbee2mqtt/th1\", \"hold_modes\": [ \"u1\", \"u2\","
+ + " \"u3\" ], \"json_attributes_template\": \"{{ value_json.attrs }}\","
+ + " \"json_attributes_topic\": \"zigbee2mqtt/th1\","
+ + " \"mode_command_template\": \"mode={{ value }}\","
+ + " \"mode_command_topic\": \"zigbee2mqtt/th1/mode\","
+ + " \"mode_state_template\": \"{{ value_json.mode }}\","
+ + " \"mode_state_topic\": \"zigbee2mqtt/th1\", \"modes\": [ \"B1\", \"B2\""
+ + " ], \"swing_command_template\": \"swing={{ value }}\","
+ + " \"swing_command_topic\": \"zigbee2mqtt/th1/swing\","
+ + " \"swing_state_template\": \"{{ value_json.swing }}\","
+ + " \"swing_state_topic\": \"zigbee2mqtt/th1\", \"swing_modes\": [ \"G1\","
+ + " \"G2\" ], \"temperature_command_template\": \"temperature={{ value }}\","
+ + " \"temperature_command_topic\": \"zigbee2mqtt/th1/temperature\","
+ + " \"temperature_state_template\": \"{{ value_json.temperature }}\","
+ + " \"temperature_state_topic\": \"zigbee2mqtt/th1\","
+ + " \"temperature_high_command_template\": \"temperature_high={{ value }}\","
+ + " \"temperature_high_command_topic\": \"zigbee2mqtt/th1/temperature_high\","
+ + " \"temperature_high_state_template\": \"{{ value_json.temperature_high }}\","
+ + " \"temperature_high_state_topic\": \"zigbee2mqtt/th1\","
+ + " \"temperature_low_command_template\": \"temperature_low={{ value }}\","
+ + " \"temperature_low_command_topic\": \"zigbee2mqtt/th1/temperature_low\","
+ + " \"temperature_low_state_template\": \"{{ value_json.temperature_low }}\","
+ + " \"temperature_low_state_topic\": \"zigbee2mqtt/th1\","
+ + " \"power_command_topic\": \"zigbee2mqtt/th1/power\", \"initial\": \"10\","
+ + " \"max_temp\": \"40\", \"min_temp\": \"0\", \"temperature_unit\": \"F\","
+ + " \"temp_step\": \"1\", \"precision\": \"0.5\", \"send_if_off\": \"false\" }");
+
+ assertThat(component.channels.size(), is(12));
+ assertThat(component.getName(), is("MQTT HVAC"));
+
+ assertChannel(component, Climate.ACTION_CH_ID, "zigbee2mqtt/th1", "", "MQTT HVAC", TextValue.class);
+ assertChannel(component, Climate.AUX_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/aux", "MQTT HVAC",
+ OnOffValue.class);
+ assertChannel(component, Climate.AWAY_MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/away_mode", "MQTT HVAC",
+ OnOffValue.class);
+ assertChannel(component, Climate.CURRENT_TEMPERATURE_CH_ID, "zigbee2mqtt/th1", "", "MQTT HVAC",
+ NumberValue.class);
+ assertChannel(component, Climate.FAN_MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/fan_mode", "MQTT HVAC",
+ TextValue.class);
+ assertChannel(component, Climate.HOLD_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/hold", "MQTT HVAC",
+ TextValue.class);
+ assertChannel(component, Climate.MODE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/mode", "MQTT HVAC",
+ TextValue.class);
+ assertChannel(component, Climate.SWING_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/swing", "MQTT HVAC",
+ TextValue.class);
+ assertChannel(component, Climate.TEMPERATURE_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/temperature",
+ "MQTT HVAC", NumberValue.class);
+ assertChannel(component, Climate.TEMPERATURE_HIGH_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/temperature_high",
+ "MQTT HVAC", NumberValue.class);
+ assertChannel(component, Climate.TEMPERATURE_LOW_CH_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/temperature_low",
+ "MQTT HVAC", NumberValue.class);
+ assertChannel(component, Climate.POWER_CH_ID, "", "zigbee2mqtt/th1/power", "MQTT HVAC", OnOffValue.class);
+
+ publishMessage("zigbee2mqtt/th1",
+ "{ \"action\": \"fan\", \"aux\": \"ON\", \"away_mode\": \"OFF\", "
+ + "\"current_temperature\": \"35.5\", \"fan_mode\": \"p2\", \"hold\": \"u2\", "
+ + "\"mode\": \"B1\", \"swing\": \"G1\", \"temperature\": \"30\", "
+ + "\"temperature_high\": \"37\", \"temperature_low\": \"20\" }");
+
+ assertState(component, Climate.ACTION_CH_ID, new StringType("fan"));
+ assertState(component, Climate.AUX_CH_ID, OnOffType.ON);
+ assertState(component, Climate.AWAY_MODE_CH_ID, OnOffType.OFF);
+ assertState(component, Climate.CURRENT_TEMPERATURE_CH_ID, new DecimalType(35.5));
+ assertState(component, Climate.FAN_MODE_CH_ID, new StringType("p2"));
+ assertState(component, Climate.HOLD_CH_ID, new StringType("u2"));
+ assertState(component, Climate.MODE_CH_ID, new StringType("B1"));
+ assertState(component, Climate.SWING_CH_ID, new StringType("G1"));
+ assertState(component, Climate.TEMPERATURE_CH_ID, new DecimalType(30));
+ assertState(component, Climate.TEMPERATURE_HIGH_CH_ID, new DecimalType(37));
+ assertState(component, Climate.TEMPERATURE_LOW_CH_ID, new DecimalType(20));
+
+ component.getChannel(Climate.AUX_CH_ID).getState().publishValue(OnOffType.OFF);
+ assertPublished("zigbee2mqtt/th1/aux", "OFF");
+ component.getChannel(Climate.AWAY_MODE_CH_ID).getState().publishValue(OnOffType.ON);
+ assertPublished("zigbee2mqtt/th1/away_mode", "ON");
+ component.getChannel(Climate.FAN_MODE_CH_ID).getState().publishValue(new StringType("p1"));
+ assertPublished("zigbee2mqtt/th1/fan_mode", "fan_mode=p1");
+ component.getChannel(Climate.HOLD_CH_ID).getState().publishValue(new StringType("u3"));
+ assertPublished("zigbee2mqtt/th1/hold", "hold=u3");
+ component.getChannel(Climate.MODE_CH_ID).getState().publishValue(new StringType("B2"));
+ assertPublished("zigbee2mqtt/th1/mode", "mode=B2");
+ component.getChannel(Climate.SWING_CH_ID).getState().publishValue(new StringType("G2"));
+ assertPublished("zigbee2mqtt/th1/swing", "swing=G2");
+ component.getChannel(Climate.TEMPERATURE_CH_ID).getState().publishValue(new DecimalType(30.5));
+ assertPublished("zigbee2mqtt/th1/temperature", "temperature=30.5");
+ component.getChannel(Climate.TEMPERATURE_HIGH_CH_ID).getState().publishValue(new DecimalType(39.5));
+ assertPublished("zigbee2mqtt/th1/temperature_high", "temperature_high=39.5");
+ component.getChannel(Climate.TEMPERATURE_LOW_CH_ID).getState().publishValue(new DecimalType(19.5));
+ assertPublished("zigbee2mqtt/th1/temperature_low", "temperature_low=19.5");
+ component.getChannel(Climate.POWER_CH_ID).getState().publishValue(OnOffType.OFF);
+ assertPublished("zigbee2mqtt/th1/power", "OFF");
+ }
+
+ protected Set getConfigTopics() {
+ return Set.of(CONFIG_TOPIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/CoverTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/CoverTests.java
new file mode 100644
index 0000000000000..c8ad1d6f1ab5f
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/CoverTests.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.generic.values.RollershutterValue;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.StopMoveType;
+
+/**
+ * Tests for {@link Cover}
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+@SuppressWarnings("ConstantConditions")
+public class CoverTests extends AbstractComponentTests {
+ public static final String CONFIG_TOPIC = "cover/0x0000000000000000_cover_zigbee2mqtt";
+
+ @Test
+ public void test() throws InterruptedException {
+ // @formatter:off
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ "{ " +
+ " \"availability\": [ " +
+ " { " +
+ " \"topic\": \"zigbee2mqtt/bridge/state\" " +
+ " } " +
+ " ], " +
+ " \"device\": { " +
+ " \"identifiers\": [ " +
+ " \"zigbee2mqtt_0x0000000000000000\" " +
+ " ], " +
+ " \"manufacturer\": \"Covers inc\", " +
+ " \"model\": \"cover v1\", " +
+ " \"name\": \"Cover\", " +
+ " \"sw_version\": \"Zigbee2MQTT 1.18.2\" " +
+ " }, " +
+ " \"name\": \"cover\", " +
+ " \"payload_open\": \"OPEN_\", " +
+ " \"payload_close\": \"CLOSE_\", " +
+ " \"payload_stop\": \"STOP_\", " +
+ " \"state_topic\": \"zigbee2mqtt/cover/state\", " +
+ " \"command_topic\": \"zigbee2mqtt/cover/set/state\" " +
+ "}");
+ // @formatter:on
+
+ assertThat(component.channels.size(), is(1));
+ assertThat(component.getName(), is("cover"));
+
+ assertChannel(component, Cover.switchChannelID, "zigbee2mqtt/cover/state", "zigbee2mqtt/cover/set/state",
+ "cover", RollershutterValue.class);
+
+ publishMessage("zigbee2mqtt/cover/state", "100");
+ assertState(component, Cover.switchChannelID, PercentType.HUNDRED);
+ publishMessage("zigbee2mqtt/cover/state", "0");
+ assertState(component, Cover.switchChannelID, PercentType.ZERO);
+
+ component.getChannel(Cover.switchChannelID).getState().publishValue(PercentType.ZERO);
+ assertPublished("zigbee2mqtt/cover/set/state", "OPEN_");
+ component.getChannel(Cover.switchChannelID).getState().publishValue(PercentType.HUNDRED);
+ assertPublished("zigbee2mqtt/cover/set/state", "CLOSE_");
+ component.getChannel(Cover.switchChannelID).getState().publishValue(StopMoveType.STOP);
+ assertPublished("zigbee2mqtt/cover/set/state", "STOP_");
+ component.getChannel(Cover.switchChannelID).getState().publishValue(PercentType.ZERO);
+ assertPublished("zigbee2mqtt/cover/set/state", "OPEN_", 2);
+ component.getChannel(Cover.switchChannelID).getState().publishValue(StopMoveType.STOP);
+ assertPublished("zigbee2mqtt/cover/set/state", "STOP_", 2);
+ }
+
+ protected Set getConfigTopics() {
+ return Set.of(CONFIG_TOPIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/FanTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/FanTests.java
new file mode 100644
index 0000000000000..34c5c7f954f54
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/FanTests.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.core.library.types.OnOffType;
+
+/**
+ * Tests for {@link Fan}
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+@SuppressWarnings("ALL")
+public class FanTests extends AbstractComponentTests {
+ public static final String CONFIG_TOPIC = "fan/0x0000000000000000_fan_zigbee2mqtt";
+
+ @Test
+ public void test() throws InterruptedException {
+ // @formatter:off
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ "{ " +
+ " \"availability\": [ " +
+ " { " +
+ " \"topic\": \"zigbee2mqtt/bridge/state\" " +
+ " } " +
+ " ], " +
+ " \"device\": { " +
+ " \"identifiers\": [ " +
+ " \"zigbee2mqtt_0x0000000000000000\" " +
+ " ], " +
+ " \"manufacturer\": \"Fans inc\", " +
+ " \"model\": \"Fan\", " +
+ " \"name\": \"FanBlower\", " +
+ " \"sw_version\": \"Zigbee2MQTT 1.18.2\" " +
+ " }, " +
+ " \"name\": \"fan\", " +
+ " \"payload_off\": \"OFF_\", " +
+ " \"payload_on\": \"ON_\", " +
+ " \"state_topic\": \"zigbee2mqtt/fan/state\", " +
+ " \"command_topic\": \"zigbee2mqtt/fan/set/state\" " +
+ "}");
+ // @formatter:on
+
+ assertThat(component.channels.size(), is(1));
+ assertThat(component.getName(), is("fan"));
+
+ assertChannel(component, Fan.switchChannelID, "zigbee2mqtt/fan/state", "zigbee2mqtt/fan/set/state", "fan",
+ OnOffValue.class);
+
+ publishMessage("zigbee2mqtt/fan/state", "ON_");
+ assertState(component, Fan.switchChannelID, OnOffType.ON);
+ publishMessage("zigbee2mqtt/fan/state", "ON_");
+ assertState(component, Fan.switchChannelID, OnOffType.ON);
+ publishMessage("zigbee2mqtt/fan/state", "OFF_");
+ assertState(component, Fan.switchChannelID, OnOffType.OFF);
+ publishMessage("zigbee2mqtt/fan/state", "ON_");
+ assertState(component, Fan.switchChannelID, OnOffType.ON);
+
+ component.getChannel(Fan.switchChannelID).getState().publishValue(OnOffType.OFF);
+ assertPublished("zigbee2mqtt/fan/set/state", "OFF_");
+ component.getChannel(Fan.switchChannelID).getState().publishValue(OnOffType.ON);
+ assertPublished("zigbee2mqtt/fan/set/state", "ON_");
+ }
+
+ protected Set getConfigTopics() {
+ return Set.of(CONFIG_TOPIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/HAConfigurationTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/HAConfigurationTests.java
new file mode 100644
index 0000000000000..e454d0d2154d8
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/HAConfigurationTests.java
@@ -0,0 +1,250 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.component;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Connection;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * @author Jochen Klein - Initial contribution
+ */
+public class HAConfigurationTests {
+
+ private Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory())
+ .create();
+
+ private static String readTestJson(final String name) {
+ StringBuilder result = new StringBuilder();
+
+ try (BufferedReader in = new BufferedReader(
+ new InputStreamReader(HAConfigurationTests.class.getResourceAsStream(name), "UTF-8"))) {
+ String line;
+
+ while ((line = in.readLine()) != null) {
+ result.append(line).append('\n');
+ }
+ return result.toString();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void testAbbreviations() {
+ String json = readTestJson("configA.json");
+
+ AbstractChannelConfiguration config = AbstractChannelConfiguration.fromString(json, gson);
+
+ assertThat(config.getName(), is("A"));
+ assertThat(config.getIcon(), is("2"));
+ assertThat(config.getQos(), is(1));
+ assertThat(config.isRetain(), is(true));
+ assertThat(config.getValueTemplate(), is("B"));
+ assertThat(config.getUniqueId(), is("C"));
+ assertThat(config.getAvailabilityTopic(), is("D/E"));
+ assertThat(config.getPayloadAvailable(), is("F"));
+ assertThat(config.getPayloadNotAvailable(), is("G"));
+
+ assertThat(config.getDevice(), is(notNullValue()));
+
+ Device device = config.getDevice();
+ if (device != null) {
+ assertThat(device.getIdentifiers(), contains("H"));
+ assertThat(device.getConnections(), is(notNullValue()));
+ List<@NonNull Connection> connections = device.getConnections();
+ if (connections != null) {
+ assertThat(connections.get(0).getType(), is("I1"));
+ assertThat(connections.get(0).getIdentifier(), is("I2"));
+ }
+ assertThat(device.getName(), is("J"));
+ assertThat(device.getModel(), is("K"));
+ assertThat(device.getSwVersion(), is("L"));
+ assertThat(device.getManufacturer(), is("M"));
+ }
+ }
+
+ @Test
+ public void testTildeSubstritution() {
+ String json = readTestJson("configB.json");
+
+ Switch.ChannelConfiguration config = AbstractChannelConfiguration.fromString(json, gson,
+ Switch.ChannelConfiguration.class);
+
+ assertThat(config.getAvailabilityTopic(), is("D/E"));
+ assertThat(config.state_topic, is("O/D/"));
+ assertThat(config.command_topic, is("P~Q"));
+ assertThat(config.getDevice(), is(notNullValue()));
+
+ Device device = config.getDevice();
+ if (device != null) {
+ assertThat(device.getIdentifiers(), contains("H"));
+ }
+ }
+
+ @Test
+ public void testSampleFanConfig() {
+ String json = readTestJson("configFan.json");
+
+ Fan.ChannelConfiguration config = AbstractChannelConfiguration.fromString(json, gson,
+ Fan.ChannelConfiguration.class);
+ assertThat(config.getName(), is("Bedroom Fan"));
+ }
+
+ @Test
+ public void testDeviceListConfig() {
+ String json = readTestJson("configDeviceList.json");
+
+ Fan.ChannelConfiguration config = AbstractChannelConfiguration.fromString(json, gson,
+ Fan.ChannelConfiguration.class);
+ assertThat(config.getDevice(), is(notNullValue()));
+
+ Device device = config.getDevice();
+ if (device != null) {
+ assertThat(device.getIdentifiers(), is(Arrays.asList("A", "B", "C")));
+ }
+ }
+
+ @Test
+ public void testDeviceSingleStringConfig() {
+ String json = readTestJson("configDeviceSingleString.json");
+
+ Fan.ChannelConfiguration config = AbstractChannelConfiguration.fromString(json, gson,
+ Fan.ChannelConfiguration.class);
+ assertThat(config.getDevice(), is(notNullValue()));
+
+ Device device = config.getDevice();
+ if (device != null) {
+ assertThat(device.getIdentifiers(), is(Arrays.asList("A")));
+ }
+ }
+
+ @Test
+ public void testTS0601ClimateConfig() {
+ String json = readTestJson("configTS0601ClimateThermostat.json");
+ Climate.ChannelConfiguration config = AbstractChannelConfiguration.fromString(json, gson,
+ Climate.ChannelConfiguration.class);
+ assertThat(config.getDevice(), is(notNullValue()));
+ assertThat(config.getDevice().getIdentifiers(), is(notNullValue()));
+ assertThat(config.getDevice().getIdentifiers().get(0), is("zigbee2mqtt_0x847127fffe11dd6a"));
+ assertThat(config.getDevice().getManufacturer(), is("TuYa"));
+ assertThat(config.getDevice().getModel(), is("Radiator valve with thermostat (TS0601_thermostat)"));
+ assertThat(config.getDevice().getName(), is("th1"));
+ assertThat(config.getDevice().getSwVersion(), is("Zigbee2MQTT 1.18.2"));
+
+ assertThat(config.action_template, is(
+ "{% set values = {'idle':'off','heat':'heating','cool':'cooling','fan only':'fan'} %}{{ values[value_json.running_state] }}"));
+ assertThat(config.action_topic, is("zigbee2mqtt/th1"));
+ assertThat(config.away_mode_command_topic, is("zigbee2mqtt/th1/set/away_mode"));
+ assertThat(config.away_mode_state_template, is("{{ value_json.away_mode }}"));
+ assertThat(config.away_mode_state_topic, is("zigbee2mqtt/th1"));
+ assertThat(config.current_temperature_template, is("{{ value_json.local_temperature }}"));
+ assertThat(config.current_temperature_topic, is("zigbee2mqtt/th1"));
+ assertThat(config.hold_command_topic, is("zigbee2mqtt/th1/set/preset"));
+ assertThat(config.hold_modes, is(List.of("schedule", "manual", "boost", "complex", "comfort", "eco")));
+ assertThat(config.hold_state_template, is("{{ value_json.preset }}"));
+ assertThat(config.hold_state_topic, is("zigbee2mqtt/th1"));
+ assertThat(config.json_attributes_topic, is("zigbee2mqtt/th1"));
+ assertThat(config.max_temp, is(35f));
+ assertThat(config.min_temp, is(5f));
+ assertThat(config.mode_command_topic, is("zigbee2mqtt/th1/set/system_mode"));
+ assertThat(config.mode_state_template, is("{{ value_json.system_mode }}"));
+ assertThat(config.mode_state_topic, is("zigbee2mqtt/th1"));
+ assertThat(config.modes, is(List.of("heat", "auto", "off")));
+ assertThat(config.getName(), is("th1"));
+ assertThat(config.temp_step, is(0.5f));
+ assertThat(config.temperature_command_topic, is("zigbee2mqtt/th1/set/current_heating_setpoint"));
+ assertThat(config.temperature_state_template, is("{{ value_json.current_heating_setpoint }}"));
+ assertThat(config.temperature_state_topic, is("zigbee2mqtt/th1"));
+ assertThat(config.temperature_unit, is("C"));
+ assertThat(config.getUniqueId(), is("0x847127fffe11dd6a_climate_zigbee2mqtt"));
+
+ assertThat(config.initial, is(21));
+ assertThat(config.send_if_off, is(true));
+ }
+
+ @Test
+ public void testClimateConfig() {
+ String json = readTestJson("configClimate.json");
+ Climate.ChannelConfiguration config = AbstractChannelConfiguration.fromString(json, gson,
+ Climate.ChannelConfiguration.class);
+ assertThat(config.action_template, is("a"));
+ assertThat(config.action_topic, is("b"));
+ assertThat(config.aux_command_topic, is("c"));
+ assertThat(config.aux_state_template, is("d"));
+ assertThat(config.aux_state_topic, is("e"));
+ assertThat(config.away_mode_command_topic, is("f"));
+ assertThat(config.away_mode_state_template, is("g"));
+ assertThat(config.away_mode_state_topic, is("h"));
+ assertThat(config.current_temperature_template, is("i"));
+ assertThat(config.current_temperature_topic, is("j"));
+ assertThat(config.fan_mode_command_template, is("k"));
+ assertThat(config.fan_mode_command_topic, is("l"));
+ assertThat(config.fan_mode_state_template, is("m"));
+ assertThat(config.fan_mode_state_topic, is("n"));
+ assertThat(config.fan_modes, is(List.of("p1", "p2")));
+ assertThat(config.hold_command_template, is("q"));
+ assertThat(config.hold_command_topic, is("r"));
+ assertThat(config.hold_state_template, is("s"));
+ assertThat(config.hold_state_topic, is("t"));
+ assertThat(config.hold_modes, is(List.of("u1", "u2", "u3")));
+ assertThat(config.json_attributes_template, is("v"));
+ assertThat(config.json_attributes_topic, is("w"));
+ assertThat(config.mode_command_template, is("x"));
+ assertThat(config.mode_command_topic, is("y"));
+ assertThat(config.mode_state_template, is("z"));
+ assertThat(config.mode_state_topic, is("A"));
+ assertThat(config.modes, is(List.of("B1", "B2")));
+ assertThat(config.swing_command_template, is("C"));
+ assertThat(config.swing_command_topic, is("D"));
+ assertThat(config.swing_state_template, is("E"));
+ assertThat(config.swing_state_topic, is("F"));
+ assertThat(config.swing_modes, is(List.of("G1")));
+ assertThat(config.temperature_command_template, is("H"));
+ assertThat(config.temperature_command_topic, is("I"));
+ assertThat(config.temperature_state_template, is("J"));
+ assertThat(config.temperature_state_topic, is("K"));
+ assertThat(config.temperature_high_command_template, is("L"));
+ assertThat(config.temperature_high_command_topic, is("N"));
+ assertThat(config.temperature_high_state_template, is("O"));
+ assertThat(config.temperature_high_state_topic, is("P"));
+ assertThat(config.temperature_low_command_template, is("Q"));
+ assertThat(config.temperature_low_command_topic, is("R"));
+ assertThat(config.temperature_low_state_template, is("S"));
+ assertThat(config.temperature_low_state_topic, is("T"));
+ assertThat(config.power_command_topic, is("U"));
+ assertThat(config.initial, is(10));
+ assertThat(config.max_temp, is(40f));
+ assertThat(config.min_temp, is(0f));
+ assertThat(config.temperature_unit, is("F"));
+ assertThat(config.temp_step, is(1f));
+ assertThat(config.precision, is(0.5f));
+ assertThat(config.send_if_off, is(false));
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/LightTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/LightTests.java
new file mode 100644
index 0000000000000..f71af007f6415
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/LightTests.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.generic.values.ColorValue;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.OnOffType;
+
+/**
+ * Tests for {@link Light}
+ * The current {@link Light} is non-compliant with the Specification and must be rewritten from scratch.
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+public class LightTests extends AbstractComponentTests {
+ public static final String CONFIG_TOPIC = "light/0x0000000000000000_light_zigbee2mqtt";
+
+ @Test
+ public void test() throws InterruptedException {
+ // @formatter:off
+ var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ "{ " +
+ " \"availability\": [ " +
+ " { " +
+ " \"topic\": \"zigbee2mqtt/bridge/state\" " +
+ " } " +
+ " ], " +
+ " \"device\": { " +
+ " \"identifiers\": [ " +
+ " \"zigbee2mqtt_0x0000000000000000\" " +
+ " ], " +
+ " \"manufacturer\": \"Lights inc\", " +
+ " \"model\": \"light v1\", " +
+ " \"name\": \"Light\", " +
+ " \"sw_version\": \"Zigbee2MQTT 1.18.2\" " +
+ " }, " +
+ " \"name\": \"light\", " +
+ " \"state_topic\": \"zigbee2mqtt/light/state\", " +
+ " \"command_topic\": \"zigbee2mqtt/light/set/state\", " +
+ " \"state_value_template\": \"{{ value_json.power }}\", " +
+ " \"payload_on\": \"ON_\", " +
+ " \"payload_off\": \"OFF_\", " +
+ " \"rgb_state_topic\": \"zigbee2mqtt/light/rgb\", " +
+ " \"rgb_command_topic\": \"zigbee2mqtt/light/set/rgb\", " +
+ " \"rgb_value_template\": \"{{ value_json.rgb }}\", " +
+ " \"brightness_state_topic\": \"zigbee2mqtt/light/brightness\", " +
+ " \"brightness_command_topic\": \"zigbee2mqtt/light/set/brightness\", " +
+ " \"brightness_value_template\": \"{{ value_json.br }}\" " +
+ "}");
+ // @formatter:on
+
+ assertThat(component.channels.size(), is(1));
+ assertThat(component.getName(), is("light"));
+
+ assertChannel(component, Light.colorChannelID, "zigbee2mqtt/light/rgb", "zigbee2mqtt/light/set/rgb", "light",
+ ColorValue.class);
+
+ assertChannel(component.switchChannel, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state", "light",
+ ColorValue.class);
+ assertChannel(component.brightnessChannel, "zigbee2mqtt/light/brightness", "zigbee2mqtt/light/set/brightness",
+ "light", ColorValue.class);
+
+ publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"255,255,255\"}");
+ assertState(component, Light.colorChannelID, HSBType.fromRGB(255, 255, 255));
+ publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"10,20,30\"}");
+ assertState(component, Light.colorChannelID, HSBType.fromRGB(10, 20, 30));
+
+ component.switchChannel.getState().publishValue(OnOffType.OFF);
+ assertPublished("zigbee2mqtt/light/set/state", "0,0,0");
+ }
+
+ protected Set getConfigTopics() {
+ return Set.of(CONFIG_TOPIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/LockTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/LockTests.java
new file mode 100644
index 0000000000000..c16672f3e1a36
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/LockTests.java
@@ -0,0 +1,120 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Set;
+
+import org.junit.Rule;
+import org.junit.jupiter.api.Test;
+import org.junit.rules.ExpectedException;
+import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.core.library.types.OnOffType;
+
+/**
+ * Tests for {@link Lock}
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+@SuppressWarnings("ALL")
+public class LockTests extends AbstractComponentTests {
+ public static final String CONFIG_TOPIC = "lock/0x0000000000000000_lock_zigbee2mqtt";
+
+ @Rule
+ public ExpectedException exceptionGrabber = ExpectedException.none();
+
+ @Test
+ public void test() throws InterruptedException {
+ // @formatter:off
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ "{ " +
+ " \"availability\": [ " +
+ " { " +
+ " \"topic\": \"zigbee2mqtt/bridge/state\" " +
+ " } " +
+ " ], " +
+ " \"device\": { " +
+ " \"identifiers\": [ " +
+ " \"zigbee2mqtt_0x0000000000000000\" " +
+ " ], " +
+ " \"manufacturer\": \"Locks inc\", " +
+ " \"model\": \"Lock\", " +
+ " \"name\": \"LockBlower\", " +
+ " \"sw_version\": \"Zigbee2MQTT 1.18.2\" " +
+ " }, " +
+ " \"name\": \"lock\", " +
+ " \"payload_unlock\": \"UNLOCK_\", " +
+ " \"payload_lock\": \"LOCK_\", " +
+ " \"state_topic\": \"zigbee2mqtt/lock/state\", " +
+ " \"command_topic\": \"zigbee2mqtt/lock/set/state\" " +
+ "}");
+ // @formatter:on
+
+ assertThat(component.channels.size(), is(1));
+ assertThat(component.getName(), is("lock"));
+
+ assertChannel(component, Lock.switchChannelID, "zigbee2mqtt/lock/state", "zigbee2mqtt/lock/set/state", "lock",
+ OnOffValue.class);
+
+ publishMessage("zigbee2mqtt/lock/state", "LOCK_");
+ assertState(component, Lock.switchChannelID, OnOffType.ON);
+ publishMessage("zigbee2mqtt/lock/state", "LOCK_");
+ assertState(component, Lock.switchChannelID, OnOffType.ON);
+ publishMessage("zigbee2mqtt/lock/state", "UNLOCK_");
+ assertState(component, Lock.switchChannelID, OnOffType.OFF);
+ publishMessage("zigbee2mqtt/lock/state", "LOCK_");
+ assertState(component, Lock.switchChannelID, OnOffType.ON);
+
+ component.getChannel(Lock.switchChannelID).getState().publishValue(OnOffType.OFF);
+ assertPublished("zigbee2mqtt/lock/set/state", "UNLOCK_");
+ component.getChannel(Lock.switchChannelID).getState().publishValue(OnOffType.ON);
+ assertPublished("zigbee2mqtt/lock/set/state", "LOCK_");
+ }
+
+ @Test
+ public void forceOptimisticIsNotSupported() {
+ exceptionGrabber.expect(UnsupportedOperationException.class);
+
+ // @formatter:off
+ publishMessage(configTopicToMqtt(CONFIG_TOPIC),
+ "{ " +
+ " \"availability\": [ " +
+ " { " +
+ " \"topic\": \"zigbee2mqtt/bridge/state\" " +
+ " } " +
+ " ], " +
+ " \"device\": { " +
+ " \"identifiers\": [ " +
+ " \"zigbee2mqtt_0x0000000000000000\" " +
+ " ], " +
+ " \"manufacturer\": \"Locks inc\", " +
+ " \"model\": \"Lock\", " +
+ " \"name\": \"LockBlower\", " +
+ " \"sw_version\": \"Zigbee2MQTT 1.18.2\" " +
+ " }, " +
+ " \"name\": \"lock\", " +
+ " \"payload_unlock\": \"UNLOCK_\", " +
+ " \"payload_lock\": \"LOCK_\", " +
+ " \"optimistic\": \"true\", " +
+ " \"state_topic\": \"zigbee2mqtt/lock/state\", " +
+ " \"command_topic\": \"zigbee2mqtt/lock/set/state\" " +
+ "}");
+ // @formatter:on
+ }
+
+ protected Set getConfigTopics() {
+ return Set.of(CONFIG_TOPIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SensorTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SensorTests.java
new file mode 100644
index 0000000000000..5f219fde81b10
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SensorTests.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.generic.values.NumberValue;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * Tests for {@link Sensor}
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+@SuppressWarnings("ConstantConditions")
+public class SensorTests extends AbstractComponentTests {
+ public static final String CONFIG_TOPIC = "sensor/0x0000000000000000_sensor_zigbee2mqtt";
+
+ @Test
+ public void test() throws InterruptedException {
+ // @formatter:off
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ "{ " +
+ " \"availability\": [ " +
+ " { " +
+ " \"topic\": \"zigbee2mqtt/bridge/state\" " +
+ " } " +
+ " ], " +
+ " \"device\": { " +
+ " \"identifiers\": [ " +
+ " \"zigbee2mqtt_0x0000000000000000\" " +
+ " ], " +
+ " \"manufacturer\": \"Sensors inc\", " +
+ " \"model\": \"Sensor\", " +
+ " \"name\": \"Sensor\", " +
+ " \"sw_version\": \"Zigbee2MQTT 1.18.2\" " +
+ " }, " +
+ " \"name\": \"sensor1\", " +
+ " \"expire_after\": \"1\", " +
+ " \"force_update\": \"true\", " +
+ " \"unit_of_measurement\": \"W\", " +
+ " \"state_topic\": \"zigbee2mqtt/sensor/state\", " +
+ " \"unique_id\": \"sn1\" " +
+ "}");
+ // @formatter:on
+
+ assertThat(component.channels.size(), is(1));
+ assertThat(component.getName(), is("sensor1"));
+ assertThat(component.getGroupUID().getId(), is("sn1"));
+
+ assertChannel(component, Sensor.sensorChannelID, "zigbee2mqtt/sensor/state", "", "sensor1", NumberValue.class);
+
+ publishMessage("zigbee2mqtt/sensor/state", "10");
+ assertState(component, Sensor.sensorChannelID, DecimalType.valueOf("10"));
+ publishMessage("zigbee2mqtt/sensor/state", "20");
+ assertState(component, Sensor.sensorChannelID, DecimalType.valueOf("20"));
+ assertThat(component.getChannel(Sensor.sensorChannelID).getState().getCache().createStateDescription(true)
+ .build().getPattern(), is("%s W"));
+
+ waitForAssert(() -> assertState(component, Sensor.sensorChannelID, UnDefType.UNDEF), 10000, 200);
+ }
+
+ protected Set getConfigTopics() {
+ return Set.of(CONFIG_TOPIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SwitchTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SwitchTests.java
new file mode 100644
index 0000000000000..975d5f2202563
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SwitchTests.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.core.library.types.OnOffType;
+
+/**
+ * Tests for {@link Switch}
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+@SuppressWarnings("ConstantConditions")
+public class SwitchTests extends AbstractComponentTests {
+ public static final String CONFIG_TOPIC = "switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt";
+
+ @Test
+ public void testSwitchWithStateAndCommand() {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ "" + "{\n" + " \"availability\": [\n" + " {\n" + " \"topic\": \"zigbee2mqtt/bridge/state\"\n"
+ + " }\n" + " ],\n" + " \"command_topic\": \"zigbee2mqtt/th1/set/auto_lock\",\n"
+ + " \"device\": {\n" + " \"identifiers\": [\n"
+ + " \"zigbee2mqtt_0x847127fffe11dd6a\"\n" + " ],\n"
+ + " \"manufacturer\": \"TuYa\",\n"
+ + " \"model\": \"Radiator valve with thermostat (TS0601_thermostat)\",\n"
+ + " \"name\": \"th1\",\n" + " \"sw_version\": \"Zigbee2MQTT 1.18.2\"\n" + " },\n"
+ + " \"json_attributes_topic\": \"zigbee2mqtt/th1\",\n" + " \"name\": \"th1 auto lock\",\n"
+ + " \"payload_off\": \"MANUAL\",\n" + " \"payload_on\": \"AUTO\",\n"
+ + " \"state_off\": \"MANUAL\",\n" + " \"state_on\": \"AUTO\",\n"
+ + " \"state_topic\": \"zigbee2mqtt/th1\",\n"
+ + " \"unique_id\": \"0x847127fffe11dd6a_auto_lock_zigbee2mqtt\",\n"
+ + " \"value_template\": \"{{ value_json.auto_lock }}\"\n" + "}");
+
+ assertThat(component.channels.size(), is(1));
+ assertThat(component.getName(), is("th1 auto lock"));
+
+ assertChannel(component, Switch.switchChannelID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/auto_lock", "state",
+ OnOffValue.class);
+
+ publishMessage("zigbee2mqtt/th1", "{\"auto_lock\": \"MANUAL\"}");
+ assertState(component, Switch.switchChannelID, OnOffType.OFF);
+ publishMessage("zigbee2mqtt/th1", "{\"auto_lock\": \"AUTO\"}");
+ assertState(component, Switch.switchChannelID, OnOffType.ON);
+
+ component.getChannel(Switch.switchChannelID).getState().publishValue(OnOffType.OFF);
+ assertPublished("zigbee2mqtt/th1/set/auto_lock", "MANUAL");
+ component.getChannel(Switch.switchChannelID).getState().publishValue(OnOffType.ON);
+ assertPublished("zigbee2mqtt/th1/set/auto_lock", "AUTO");
+ }
+
+ @Test
+ public void testSwitchWithState() {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ "" + "{\n" + " \"availability\": [\n" + " {\n" + " \"topic\": \"zigbee2mqtt/bridge/state\"\n"
+ + " }\n" + " ],\n" + " \"device\": {\n" + " \"identifiers\": [\n"
+ + " \"zigbee2mqtt_0x847127fffe11dd6a\"\n" + " ],\n"
+ + " \"manufacturer\": \"TuYa\",\n"
+ + " \"model\": \"Radiator valve with thermostat (TS0601_thermostat)\",\n"
+ + " \"name\": \"th1\",\n" + " \"sw_version\": \"Zigbee2MQTT 1.18.2\"\n" + " },\n"
+ + " \"json_attributes_topic\": \"zigbee2mqtt/th1\",\n" + " \"name\": \"th1 auto lock\",\n"
+ + " \"state_off\": \"MANUAL\",\n" + " \"state_on\": \"AUTO\",\n"
+ + " \"state_topic\": \"zigbee2mqtt/th1\",\n"
+ + " \"unique_id\": \"0x847127fffe11dd6a_auto_lock_zigbee2mqtt\",\n"
+ + " \"value_template\": \"{{ value_json.auto_lock }}\"\n" + "}");
+
+ assertThat(component.channels.size(), is(1));
+ assertThat(component.getName(), is("th1 auto lock"));
+
+ assertChannel(component, Switch.switchChannelID, "zigbee2mqtt/th1", "", "state", OnOffValue.class);
+
+ publishMessage("zigbee2mqtt/th1", "{\"auto_lock\": \"MANUAL\"}");
+ assertState(component, Switch.switchChannelID, OnOffType.OFF);
+ publishMessage("zigbee2mqtt/th1", "{\"auto_lock\": \"AUTO\"}");
+ assertState(component, Switch.switchChannelID, OnOffType.ON);
+ }
+
+ @Test
+ public void testSwitchWithCommand() {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ "" + "{\n" + " \"availability\": [\n" + " {\n" + " \"topic\": \"zigbee2mqtt/bridge/state\"\n"
+ + " }\n" + " ],\n" + " \"command_topic\": \"zigbee2mqtt/th1/set/auto_lock\",\n"
+ + " \"device\": {\n" + " \"identifiers\": [\n"
+ + " \"zigbee2mqtt_0x847127fffe11dd6a\"\n" + " ],\n"
+ + " \"manufacturer\": \"TuYa\",\n"
+ + " \"model\": \"Radiator valve with thermostat (TS0601_thermostat)\",\n"
+ + " \"name\": \"th1\",\n" + " \"sw_version\": \"Zigbee2MQTT 1.18.2\"\n" + " },\n"
+ + " \"json_attributes_topic\": \"zigbee2mqtt/th1\",\n" + " \"name\": \"th1 auto lock\",\n"
+ + " \"payload_off\": \"MANUAL\",\n" + " \"payload_on\": \"AUTO\",\n"
+ + " \"unique_id\": \"0x847127fffe11dd6a_auto_lock_zigbee2mqtt\",\n"
+ + " \"value_template\": \"{{ value_json.auto_lock }}\"\n" + "}");
+
+ assertThat(component.channels.size(), is(1));
+ assertThat(component.getName(), is("th1 auto lock"));
+
+ assertChannel(component, Switch.switchChannelID, "", "zigbee2mqtt/th1/set/auto_lock", "state",
+ OnOffValue.class);
+
+ component.getChannel(Switch.switchChannelID).getState().publishValue(OnOffType.OFF);
+ assertPublished("zigbee2mqtt/th1/set/auto_lock", "MANUAL");
+ component.getChannel(Switch.switchChannelID).getState().publishValue(OnOffType.ON);
+ assertPublished("zigbee2mqtt/th1/set/auto_lock", "AUTO");
+ }
+
+ protected Set getConfigTopics() {
+ return Set.of(CONFIG_TOPIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscoveryTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscoveryTests.java
new file mode 100644
index 0000000000000..4218114d1e93e
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscoveryTests.java
@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.discovery;
+
+import static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+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.junit.jupiter.MockitoExtension;
+import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
+import org.openhab.binding.mqtt.homeassistant.internal.AbstractHomeAssistantTests;
+import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
+import org.openhab.core.config.discovery.DiscoveryListener;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * Tests for {@link HomeAssistantDiscovery}
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+@SuppressWarnings({ "ConstantConditions", "unchecked" })
+@ExtendWith(MockitoExtension.class)
+public class HomeAssistantDiscoveryTests extends AbstractHomeAssistantTests {
+ private HomeAssistantDiscovery discovery;
+
+ @BeforeEach
+ public void beforeEach() {
+ discovery = new TestHomeAssistantDiscovery(channelTypeProvider);
+ }
+
+ @Test
+ public void testOneThingDiscovery() throws Exception {
+ var discoveryListener = new LatchDiscoveryListener();
+ var latch = discoveryListener.createWaitForThingsDiscoveredLatch(1);
+
+ // When discover one thing with two channels
+ discovery.addDiscoveryListener(discoveryListener);
+ discovery.receivedMessage(HA_UID, bridgeConnection,
+ "homeassistant/climate/0x847127fffe11dd6a_climate_zigbee2mqtt/config",
+ getResourceAsByteArray("component/configTS0601ClimateThermostat.json"));
+ discovery.receivedMessage(HA_UID, bridgeConnection,
+ "homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config",
+ getResourceAsByteArray("component/configTS0601AutoLock.json"));
+
+ // Then one thing found
+ assert latch.await(3, TimeUnit.SECONDS);
+ var discoveryResults = discoveryListener.getDiscoveryResults();
+ assertThat(discoveryResults.size(), is(1));
+ var result = discoveryResults.get(0);
+ assertThat(result.getBridgeUID(), is(HA_UID));
+ assertThat(result.getProperties().get(Thing.PROPERTY_MODEL_ID),
+ is("Radiator valve with thermostat (TS0601_thermostat)"));
+ assertThat(result.getProperties().get(Thing.PROPERTY_VENDOR), is("TuYa"));
+ assertThat(result.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is("Zigbee2MQTT 1.18.2"));
+ assertThat(result.getProperties().get(HandlerConfiguration.PROPERTY_BASETOPIC), is("homeassistant"));
+ assertThat((List) result.getProperties().get(HandlerConfiguration.PROPERTY_TOPICS), hasItems(
+ "climate/0x847127fffe11dd6a_climate_zigbee2mqtt", "switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt"));
+ }
+
+ private static class TestHomeAssistantDiscovery extends HomeAssistantDiscovery {
+ public TestHomeAssistantDiscovery(MqttChannelTypeProvider typeProvider) {
+ this.typeProvider = typeProvider;
+ }
+ }
+
+ @NonNullByDefault
+ private static class LatchDiscoveryListener implements DiscoveryListener {
+ private final CopyOnWriteArrayList discoveryResults = new CopyOnWriteArrayList<>();
+ private @Nullable CountDownLatch latch;
+
+ public void thingDiscovered(DiscoveryService source, DiscoveryResult result) {
+ discoveryResults.add(result);
+ if (latch != null) {
+ latch.countDown();
+ }
+ }
+
+ public void thingRemoved(DiscoveryService source, ThingUID thingUID) {
+ }
+
+ public @Nullable Collection removeOlderResults(DiscoveryService source, long timestamp,
+ @Nullable Collection thingTypeUIDs, @Nullable ThingUID bridgeUID) {
+ return Collections.emptyList();
+ }
+
+ public CopyOnWriteArrayList getDiscoveryResults() {
+ return discoveryResults;
+ }
+
+ public CountDownLatch createWaitForThingsDiscoveredLatch(int count) {
+ final var newLatch = new CountDownLatch(count);
+ latch = newLatch;
+ return newLatch;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java
new file mode 100644
index 0000000000000..2a4d451fd48df
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java
@@ -0,0 +1,155 @@
+/**
+ * Copyright (c) 2010-2021 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.internal.handler;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.hamcrest.CoreMatchers;
+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.openhab.binding.mqtt.homeassistant.internal.AbstractHomeAssistantTests;
+import org.openhab.binding.mqtt.homeassistant.internal.HaID;
+import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
+import org.openhab.binding.mqtt.homeassistant.internal.component.Climate;
+import org.openhab.binding.mqtt.homeassistant.internal.component.Switch;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+
+/**
+ * Tests for {@link HomeAssistantThingHandler}
+ *
+ * @author Anton Kharuzhy - Initial contribution
+ */
+@SuppressWarnings({ "ConstantConditions" })
+@ExtendWith(MockitoExtension.class)
+public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
+ private final static int SUBSCRIBE_TIMEOUT = 10000;
+ private final static int ATTRIBUTE_RECEIVE_TIMEOUT = 2000;
+
+ private static final List CONFIG_TOPICS = Arrays.asList("climate/0x847127fffe11dd6a_climate_zigbee2mqtt",
+ "switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt",
+
+ "sensor/0x1111111111111111_test_sensor_zigbee2mqtt", "camera/0x1111111111111111_test_camera_zigbee2mqtt",
+
+ "cover/0x2222222222222222_test_cover_zigbee2mqtt", "fan/0x2222222222222222_test_fan_zigbee2mqtt",
+ "light/0x2222222222222222_test_light_zigbee2mqtt", "lock/0x2222222222222222_test_lock_zigbee2mqtt");
+
+ private static final List MQTT_TOPICS = CONFIG_TOPICS.stream()
+ .map(AbstractHomeAssistantTests::configTopicToMqtt).collect(Collectors.toList());
+
+ private @Mock ThingHandlerCallback callback;
+ private HomeAssistantThingHandler thingHandler;
+
+ @BeforeEach
+ public void setup() {
+ final var config = haThing.getConfiguration();
+
+ config.put(HandlerConfiguration.PROPERTY_BASETOPIC, HandlerConfiguration.DEFAULT_BASETOPIC);
+ config.put(HandlerConfiguration.PROPERTY_TOPICS, CONFIG_TOPICS);
+
+ when(callback.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
+
+ thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, transformationServiceProvider,
+ SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
+ thingHandler.setConnection(bridgeConnection);
+ thingHandler.setCallback(callback);
+ thingHandler = spy(thingHandler);
+ }
+
+ @Test
+ public void testInitialize() {
+ // When initialize
+ thingHandler.initialize();
+
+ verify(callback).statusUpdated(eq(haThing), any());
+ // Expect a call to the bridge status changed, the start, the propertiesChanged method
+ verify(thingHandler).bridgeStatusChanged(any());
+ verify(thingHandler, timeout(SUBSCRIBE_TIMEOUT)).start(any());
+
+ // Expect subscription on each topic from config
+ MQTT_TOPICS.forEach(t -> {
+ verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).subscribe(eq(t), any());
+ });
+
+ verify(thingHandler, never()).componentDiscovered(any(), any());
+ assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
+ // Components discovered after messages in corresponding topics
+ var configTopic = "homeassistant/climate/0x847127fffe11dd6a_climate_zigbee2mqtt/config";
+ thingHandler.discoverComponents.processMessage(configTopic,
+ getResourceAsByteArray("component/configTS0601ClimateThermostat.json"));
+ verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopic)), any(Climate.class));
+
+ thingHandler.delayedProcessing.forceProcessNow();
+ assertThat(haThing.getChannels().size(), CoreMatchers.is(6));
+ verify(channelTypeProvider, times(6)).setChannelType(any(), any());
+ verify(channelTypeProvider, times(1)).setChannelGroupType(any(), any());
+
+ configTopic = "homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config";
+ thingHandler.discoverComponents.processMessage(configTopic,
+ getResourceAsByteArray("component/configTS0601AutoLock.json"));
+ verify(thingHandler, times(2)).componentDiscovered(any(), any());
+ verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopic)), any(Switch.class));
+
+ thingHandler.delayedProcessing.forceProcessNow();
+ assertThat(haThing.getChannels().size(), CoreMatchers.is(7));
+ verify(channelTypeProvider, times(7)).setChannelType(any(), any());
+ verify(channelTypeProvider, times(2)).setChannelGroupType(any(), any());
+ }
+
+ @Test
+ public void testDispose() {
+ thingHandler.initialize();
+
+ // Expect subscription on each topic from config
+ CONFIG_TOPICS.forEach(t -> {
+ var fullTopic = HandlerConfiguration.DEFAULT_BASETOPIC + "/" + t + "/config";
+ verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).subscribe(eq(fullTopic), any());
+ });
+ thingHandler.discoverComponents.processMessage(
+ "homeassistant/climate/0x847127fffe11dd6a_climate_zigbee2mqtt/config",
+ getResourceAsByteArray("component/configTS0601ClimateThermostat.json"));
+ thingHandler.discoverComponents.processMessage(
+ "homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config",
+ getResourceAsByteArray("component/configTS0601AutoLock.json"));
+ thingHandler.delayedProcessing.forceProcessNow();
+ assertThat(haThing.getChannels().size(), CoreMatchers.is(7));
+ verify(channelTypeProvider, times(7)).setChannelType(any(), any());
+
+ // When dispose
+ thingHandler.dispose();
+
+ // Expect unsubscription on each topic from config
+ MQTT_TOPICS.forEach(t -> {
+ verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).unsubscribe(eq(t), any());
+ });
+
+ // Expect channel types removed, 6 for climate and 1 for switch
+ verify(channelTypeProvider, times(7)).removeChannelType(any());
+ // Expect channel group types removed, 1 for each component
+ verify(channelTypeProvider, times(2)).removeChannelGroupType(any());
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/configA.json b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configA.json
similarity index 100%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/configA.json
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configA.json
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/configB.json b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configB.json
similarity index 100%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/configB.json
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configB.json
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configClimate.json b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configClimate.json
new file mode 100644
index 0000000000000..671b0b65e01cf
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configClimate.json
@@ -0,0 +1,66 @@
+{
+ "action_template": "a",
+ "action_topic": "b",
+ "aux_command_topic": "c",
+ "aux_state_template": "d",
+ "aux_state_topic": "e",
+ "away_mode_command_topic": "f",
+ "away_mode_state_template": "g",
+ "away_mode_state_topic": "h",
+ "current_temperature_template": "i",
+ "current_temperature_topic": "j",
+ "fan_mode_command_template": "k",
+ "fan_mode_command_topic": "l",
+ "fan_mode_state_template": "m",
+ "fan_mode_state_topic": "n",
+ "fan_modes": [
+ "p1",
+ "p2"
+ ],
+ "hold_command_template": "q",
+ "hold_command_topic": "r",
+ "hold_state_template": "s",
+ "hold_state_topic": "t",
+ "hold_modes": [
+ "u1",
+ "u2",
+ "u3"
+ ],
+ "json_attributes_template": "v",
+ "json_attributes_topic": "w",
+ "mode_command_template": "x",
+ "mode_command_topic": "y",
+ "mode_state_template": "z",
+ "mode_state_topic": "A",
+ "modes": [
+ "B1",
+ "B2"
+ ],
+ "swing_command_template": "C",
+ "swing_command_topic": "D",
+ "swing_state_template": "E",
+ "swing_state_topic": "F",
+ "swing_modes": [
+ "G1"
+ ],
+ "temperature_command_template": "H",
+ "temperature_command_topic": "I",
+ "temperature_state_template": "J",
+ "temperature_state_topic": "K",
+ "temperature_high_command_template": "L",
+ "temperature_high_command_topic": "N",
+ "temperature_high_state_template": "O",
+ "temperature_high_state_topic": "P",
+ "temperature_low_command_template": "Q",
+ "temperature_low_command_topic": "R",
+ "temperature_low_state_template": "S",
+ "temperature_low_state_topic": "T",
+ "power_command_topic": "U",
+ "initial": "10",
+ "max_temp": "40",
+ "min_temp": "0",
+ "temperature_unit": "F",
+ "temp_step": "1",
+ "precision": "0.5",
+ "send_if_off": "false"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/configDeviceList.json b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configDeviceList.json
similarity index 100%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/configDeviceList.json
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configDeviceList.json
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/configDeviceSingleString.json b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configDeviceSingleString.json
similarity index 100%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/configDeviceSingleString.json
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configDeviceSingleString.json
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/configFan.json b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configFan.json
similarity index 100%
rename from bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/configFan.json
rename to bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configFan.json
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configTS0601AutoLock.json b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configTS0601AutoLock.json
new file mode 100644
index 0000000000000..2ad9c4410d3fe
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configTS0601AutoLock.json
@@ -0,0 +1,26 @@
+{
+ "availability": [
+ {
+ "topic": "zigbee2mqtt/bridge/state"
+ }
+ ],
+ "command_topic": "zigbee2mqtt/th1/set/auto_lock",
+ "device": {
+ "identifiers": [
+ "zigbee2mqtt_0x847127fffe11dd6a"
+ ],
+ "manufacturer": "TuYa",
+ "model": "Radiator valve with thermostat (TS0601_thermostat)",
+ "name": "th1",
+ "sw_version": "Zigbee2MQTT 1.18.2"
+ },
+ "json_attributes_topic": "zigbee2mqtt/th1",
+ "name": "th1 auto lock",
+ "payload_off": "MANUAL",
+ "payload_on": "AUTO",
+ "state_off": "MANUAL",
+ "state_on": "AUTO",
+ "state_topic": "zigbee2mqtt/th1",
+ "unique_id": "0x847127fffe11dd6a_auto_lock_zigbee2mqtt",
+ "value_template": "{{ value_json.auto_lock }}"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configTS0601ClimateThermostat.json b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configTS0601ClimateThermostat.json
new file mode 100644
index 0000000000000..4ab5d2e391739
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/configTS0601ClimateThermostat.json
@@ -0,0 +1,52 @@
+{
+ "action_template": "{% set values = {'idle':'off','heat':'heating','cool':'cooling','fan only':'fan'} %}{{ values[value_json.running_state] }}",
+ "action_topic": "zigbee2mqtt/th1",
+ "availability": [
+ {
+ "topic": "zigbee2mqtt/bridge/state"
+ }
+ ],
+ "away_mode_command_topic": "zigbee2mqtt/th1/set/away_mode",
+ "away_mode_state_template": "{{ value_json.away_mode }}",
+ "away_mode_state_topic": "zigbee2mqtt/th1",
+ "current_temperature_template": "{{ value_json.local_temperature }}",
+ "current_temperature_topic": "zigbee2mqtt/th1",
+ "device": {
+ "identifiers": [
+ "zigbee2mqtt_0x847127fffe11dd6a"
+ ],
+ "manufacturer": "TuYa",
+ "model": "Radiator valve with thermostat (TS0601_thermostat)",
+ "name": "th1",
+ "sw_version": "Zigbee2MQTT 1.18.2"
+ },
+ "hold_command_topic": "zigbee2mqtt/th1/set/preset",
+ "hold_modes": [
+ "schedule",
+ "manual",
+ "boost",
+ "complex",
+ "comfort",
+ "eco"
+ ],
+ "hold_state_template": "{{ value_json.preset }}",
+ "hold_state_topic": "zigbee2mqtt/th1",
+ "json_attributes_topic": "zigbee2mqtt/th1",
+ "max_temp": "35",
+ "min_temp": "5",
+ "mode_command_topic": "zigbee2mqtt/th1/set/system_mode",
+ "mode_state_template": "{{ value_json.system_mode }}",
+ "mode_state_topic": "zigbee2mqtt/th1",
+ "modes": [
+ "heat",
+ "auto",
+ "off"
+ ],
+ "name": "th1",
+ "temp_step": 0.5,
+ "temperature_command_topic": "zigbee2mqtt/th1/set/current_heating_setpoint",
+ "temperature_state_template": "{{ value_json.current_heating_setpoint }}",
+ "temperature_state_topic": "zigbee2mqtt/th1",
+ "temperature_unit": "C",
+ "unique_id": "0x847127fffe11dd6a_climate_zigbee2mqtt"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/image.png b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/image.png
new file mode 100644
index 0000000000000..100bcfd570b5e
Binary files /dev/null and b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/resources/org/openhab/binding/mqtt/homeassistant/internal/component/image.png differ