diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml b/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml index eb7e4052698a9..d4528d7ec8738 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml +++ b/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml @@ -27,5 +27,12 @@ ${project.version} provided + + + org.openhab.addons.bundles + org.openhab.transform.jinja + ${project.version} + test + diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/CChannel.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java similarity index 78% rename from bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/CChannel.java rename to bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java index cffc7027d25b0..61d557f0472e3 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/CChannel.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java @@ -15,6 +15,7 @@ import java.net.URI; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Predicate; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -26,7 +27,7 @@ import org.openhab.binding.mqtt.generic.TransformationServiceProvider; 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.component.AbstractComponent; import org.openhab.core.config.core.Configuration; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.thing.Channel; @@ -37,6 +38,7 @@ import org.openhab.core.thing.type.ChannelType; import org.openhab.core.thing.type.ChannelTypeBuilder; import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.Command; import org.openhab.core.types.StateDescriptionFragment; /** @@ -55,7 +57,7 @@ * @author David Graeff - Initial contribution */ @NonNullByDefault -public class CChannel { +public class ComponentChannel { private static final String JINJA = "JINJA"; private final ChannelUID channelUID; @@ -65,7 +67,7 @@ public class CChannel { private final ChannelTypeUID channelTypeUID; private final ChannelStateUpdateListener channelStateUpdateListener; - private CChannel(ChannelUID channelUID, ChannelState channelState, Channel channel, ChannelType type, + private ComponentChannel(ChannelUID channelUID, ChannelState channelState, Channel channel, ChannelType type, ChannelTypeUID channelTypeUID, ChannelStateUpdateListener channelStateUpdateListener) { super(); this.channelUID = channelUID; @@ -117,24 +119,25 @@ public void resetState() { } public static class Builder { - private AbstractComponent component; - private ComponentConfiguration componentConfiguration; - private String channelID; - private Value valueState; - private String label; + private final AbstractComponent component; + private final String channelID; + private final Value valueState; + private final String label; + private final ChannelStateUpdateListener channelStateUpdateListener; + private @Nullable String state_topic; private @Nullable String command_topic; private boolean retain; private boolean trigger; private @Nullable Integer qos; - private ChannelStateUpdateListener channelStateUpdateListener; + private @Nullable Predicate commandFilter; private @Nullable String templateIn; + private @Nullable String templateOut; - public Builder(AbstractComponent component, ComponentConfiguration componentConfiguration, String channelID, - Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) { + public Builder(AbstractComponent component, String channelID, Value valueState, String label, + ChannelStateUpdateListener channelStateUpdateListener) { this.component = component; - this.componentConfiguration = componentConfiguration; this.channelID = channelID; this.valueState = valueState; this.label = label; @@ -161,9 +164,9 @@ public Builder stateTopic(@Nullable String state_topic, @Nullable String... temp /** * @deprecated use commandTopic(String, boolean, int) - * @param command_topic - * @param retain - * @return + * @param command_topic topic + * @param retain retain + * @return this */ @Deprecated public Builder commandTopic(@Nullable String command_topic, boolean retain) { @@ -173,9 +176,17 @@ public Builder commandTopic(@Nullable String command_topic, boolean retain) { } public Builder commandTopic(@Nullable String command_topic, boolean retain, int qos) { + return commandTopic(command_topic, retain, qos, null); + } + + public Builder commandTopic(@Nullable String command_topic, boolean retain, int qos, + @Nullable String template) { this.command_topic = command_topic; this.retain = retain; this.qos = qos; + if (command_topic != null && !command_topic.isBlank()) { + this.templateOut = template; + } return this; } @@ -184,24 +195,29 @@ public Builder trigger(boolean trigger) { return this; } - public CChannel build() { + public Builder commandFilter(@Nullable Predicate commandFilter) { + this.commandFilter = commandFilter; + return this; + } + + public ComponentChannel build() { return build(true); } - public CChannel build(boolean addToComponent) { + public ComponentChannel build(boolean addToComponent) { ChannelUID channelUID; ChannelState channelState; Channel channel; ChannelType type; ChannelTypeUID channelTypeUID; - channelUID = new ChannelUID(component.channelGroupUID, channelID); + channelUID = new ChannelUID(component.getGroupUID(), channelID); channelTypeUID = new ChannelTypeUID(MqttBindingConstants.BINDING_ID, channelUID.getGroupId() + "_" + channelID); - channelState = new ChannelState( + channelState = new HomeAssistantChannelState( ChannelConfigBuilder.create().withRetain(retain).withQos(qos).withStateTopic(state_topic) .withCommandTopic(command_topic).makeTrigger(trigger).build(), - channelUID, valueState, channelStateUpdateListener); + channelUID, valueState, channelStateUpdateListener, commandFilter); String localStateTopic = state_topic; if (localStateTopic == null || localStateTopic.isBlank() || this.trigger) { @@ -215,26 +231,29 @@ public CChannel build(boolean addToComponent) { } Configuration configuration = new Configuration(); - configuration.put("config", component.channelConfigurationJson); - component.haID.toConfig(configuration); + configuration.put("config", component.getChannelConfigurationJson()); + component.getHaID().toConfig(configuration); channel = ChannelBuilder.create(channelUID, channelState.getItemType()).withType(channelTypeUID) .withKind(type.getKind()).withLabel(label).withConfiguration(configuration).build(); - CChannel result = new CChannel(channelUID, channelState, channel, type, channelTypeUID, + ComponentChannel result = new ComponentChannel(channelUID, channelState, channel, type, channelTypeUID, channelStateUpdateListener); - @Nullable - TransformationServiceProvider transformationProvider = componentConfiguration - .getTransformationServiceProvider(); + TransformationServiceProvider transformationProvider = component.getTransformationServiceProvider(); final String templateIn = this.templateIn; if (templateIn != null && transformationProvider != null) { channelState .addTransformation(new ChannelStateTransformation(JINJA, templateIn, transformationProvider)); } + final String templateOut = this.templateOut; + if (templateOut != null && transformationProvider != null) { + channelState.addTransformationOut( + new ChannelStateTransformation(JINJA, templateOut, transformationProvider)); + } if (addToComponent) { - component.channels.put(channelID, result); + component.getChannelMap().put(channelID, result); } return result; } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentClimate.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentClimate.java deleted file mode 100644 index 9e5b5d32cf0fd..0000000000000 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentClimate.java +++ /dev/null @@ -1,40 +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 org.eclipse.jdt.annotation.NonNullByDefault; - -/** - * A MQTT climate component, following the https://www.home-assistant.io/components/climate.mqtt/ specification. - * - * At the moment this only notifies the user that this feature is not yet supported. - * - * @author David Graeff - Initial contribution - */ -@NonNullByDefault -public class ComponentClimate extends AbstractComponent { - - /** - * Configuration class for MQTT component - */ - static class ChannelConfiguration extends BaseChannelConfiguration { - ChannelConfiguration() { - super("MQTT HVAC"); - } - } - - public ComponentClimate(CFactory.ComponentConfiguration componentConfiguration) { - super(componentConfiguration, ChannelConfiguration.class); - throw new UnsupportedOperationException("Component:Climate not supported yet"); - } -} diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java index 3e6794b58fed8..a24c5cabb418a 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java @@ -27,6 +27,8 @@ import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; import org.openhab.binding.mqtt.generic.TransformationServiceProvider; import org.openhab.binding.mqtt.generic.utils.FutureCollector; +import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent; +import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber; import org.openhab.core.thing.ThingUID; @@ -55,7 +57,7 @@ public class DiscoverComponents implements MqttMessageSubscriber { private @Nullable ScheduledFuture stopDiscoveryFuture; private WeakReference<@Nullable MqttBrokerConnection> connectionRef = new WeakReference<>(null); - protected @NonNullByDefault({}) ComponentDiscovered discoveredListener; + protected @Nullable ComponentDiscovered discoveredListener; private int discoverTime; private Set 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 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 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