[mqtt-homeassistant] climate.mqtt support (#10690)
* MQTT.Homeassistant Climate support Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant synthetic config test added Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant refactoring Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant discovery test added Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant thing handler test added Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant switch test added Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant Climate test added Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant author header added Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant copyright header added Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant test fixed Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant test fixed Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant test infrastructure updated. Added tests with mqtt publishing and commands posting. Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant fixed Climate#send_if_off handling Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant do not filter the power command Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant climate unit test added Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * Update bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java Redundant annotation removed Co-authored-by: Fabian Wolter <github@fabian-wolter.de> * MQTT.Homeassistant Redundant @Nullable annotations removed Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant Unit tests added for all components Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant Unit tests stability fix Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant @NonNullByDefault removed from Device, config.dto package created Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant Climate author added Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant Device.sw_version renamed Signed-off-by: Anton Kharuzhy <antroids@gmail.com> * MQTT.Homeassistant tests wait timeout increased to 10s Signed-off-by: Anton Kharuzhy <antroids@gmail.com> Co-authored-by: antroids <antroids@gmail.com> Co-authored-by: Fabian Wolter <github@fabian-wolter.de>pull/11119/head
parent
9f09db1f18
commit
3a7835e122
|
@ -27,5 +27,12 @@
|
|||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.transform.jinja</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
|
@ -15,6 +15,7 @@ package org.openhab.binding.mqtt.homeassistant.internal;
|
|||
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.MqttChannelTypeProvider;
|
|||
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.ChannelDefinitionBuilder;
|
|||
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 @@ import org.openhab.core.types.StateDescriptionFragment;
|
|||
* @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 class CChannel {
|
|||
}
|
||||
|
||||
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<Command> 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 class CChannel {
|
|||
|
||||
/**
|
||||
* @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 class CChannel {
|
|||
}
|
||||
|
||||
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 class CChannel {
|
|||
return this;
|
||||
}
|
||||
|
||||
public CChannel build() {
|
||||
public Builder commandFilter(@Nullable Predicate<Command> 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 class CChannel {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -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<ComponentClimate.ChannelConfiguration> {
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
}
|
|
@ -27,6 +27,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.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<String> topics = new HashSet<>();
|
||||
|
||||
|
@ -92,12 +94,11 @@ public class DiscoverComponents implements MqttMessageSubscriber {
|
|||
|
||||
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 class DiscoverComponents implements MqttMessageSubscriber {
|
|||
* @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 @@ public class DiscoverComponents implements MqttMessageSubscriber {
|
|||
|
||||
/**
|
||||
* 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"));
|
||||
|
|
|
@ -89,7 +89,7 @@ public class HaID {
|
|||
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 @@ public class HaID {
|
|||
* <p>
|
||||
* <code>objectid</code>, <code>nodeid</code>, and <code>component</code> 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 class HaID {
|
|||
* <p>
|
||||
* <code>objectid</code>, <code>nodeid</code>, and <code>component</code> 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 class HaID {
|
|||
* The <code>component</code> component in the resulting HaID will be set to <code>+</code>.
|
||||
* This enables the HaID to be used as an mqtt subscription topic.
|
||||
*
|
||||
* @param config
|
||||
* @param config config
|
||||
* @return newly created HaID
|
||||
*/
|
||||
public static Collection<HaID> fromConfig(HandlerConfiguration config) {
|
||||
|
|
|
@ -28,6 +28,9 @@ import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThin
|
|||
*/
|
||||
@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 <code>getConfigAs</code> will not work.
|
||||
* The MQTT prefix topic
|
||||
|
@ -64,7 +67,7 @@ public class HandlerConfiguration {
|
|||
public List<String> topics;
|
||||
|
||||
public HandlerConfiguration() {
|
||||
this("homeassistant", Collections.emptyList());
|
||||
this(DEFAULT_BASETOPIC, Collections.emptyList());
|
||||
}
|
||||
|
||||
public HandlerConfiguration(String basetopic, List<String> topics) {
|
||||
|
@ -76,12 +79,12 @@ public class HandlerConfiguration {
|
|||
/**
|
||||
* Add the <code>basetopic</code> and <code>objectid</code> to the properties.
|
||||
*
|
||||
* @param properties
|
||||
* @param properties properties
|
||||
* @return the modified properties
|
||||
*/
|
||||
public <T extends Map<String, Object>> T appendToProperties(T properties) {
|
||||
properties.put("basetopic", basetopic);
|
||||
properties.put("topics", topics);
|
||||
properties.put(PROPERTY_BASETOPIC, basetopic);
|
||||
properties.put(PROPERTY_TOPICS, topics);
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Command> 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 <code>true</code> command will be published, on
|
||||
* <code>false</code> ignored. Can be <code>null</code> to publish all commands.
|
||||
*/
|
||||
public HomeAssistantChannelState(ChannelConfig config, ChannelUID channelUID, Value cachedValue,
|
||||
@Nullable ChannelStateUpdateListener channelStateUpdateListener,
|
||||
@Nullable Predicate<Command> commandFilter) {
|
||||
super(config, channelUID, cachedValue, channelStateUpdateListener);
|
||||
this.commandFilter = commandFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> 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);
|
||||
}
|
||||
}
|
|
@ -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.NonNullByDefault;
|
|||
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 @@ import org.openhab.core.thing.type.ChannelGroupTypeUID;
|
|||
* It has a name and consists of multiple channels.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
* @param <C> Config class derived from {@link BaseChannelConfiguration}
|
||||
* @param <C> Config class derived from {@link AbstractChannelConfiguration}
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
|
||||
public abstract class AbstractComponent<C extends AbstractChannelConfiguration> {
|
||||
// Component location fields
|
||||
private final ComponentConfiguration componentConfiguration;
|
||||
protected final ChannelGroupTypeUID channelGroupTypeUID;
|
||||
|
@ -51,7 +55,7 @@ public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
|
|||
protected final HaID haID;
|
||||
|
||||
// Channels and configuration
|
||||
protected final Map<String, CChannel> channels = new TreeMap<>();
|
||||
protected final Map<String, ComponentChannel> 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<C extends BaseChannelConfiguration> {
|
|||
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<C> clazz) {
|
||||
public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz) {
|
||||
this.componentConfiguration = componentConfiguration;
|
||||
|
||||
this.channelConfigurationJson = componentConfiguration.getConfigJSON();
|
||||
|
@ -77,24 +79,24 @@ public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
|
|||
|
||||
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 abstract class AbstractComponent<C extends BaseChannelConfiguration> {
|
|||
/**
|
||||
* 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 abstract class AbstractComponent<C extends BaseChannelConfiguration> {
|
|||
* 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 abstract class AbstractComponent<C extends BaseChannelConfiguration> {
|
|||
* @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 abstract class AbstractComponent<C extends BaseChannelConfiguration> {
|
|||
*/
|
||||
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<String, CChannel> channelTypes() {
|
||||
public Map<String, ComponentChannel> 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 abstract class AbstractComponent<C extends BaseChannelConfiguration> {
|
|||
/**
|
||||
* Return the channel group type.
|
||||
*/
|
||||
public ChannelGroupType type() {
|
||||
final List<ChannelDefinition> channelDefinitions = channels.values().stream().map(CChannel::type)
|
||||
public ChannelGroupType getType() {
|
||||
final List<ChannelDefinition> 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 abstract class AbstractComponent<C extends BaseChannelConfiguration> {
|
|||
* 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();
|
||||
}
|
||||
}
|
|
@ -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 @@ import org.openhab.binding.mqtt.generic.values.TextValue;
|
|||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ComponentAlarmControlPanel extends AbstractComponent<ComponentAlarmControlPanel.ChannelConfiguration> {
|
||||
public class AlarmControlPanel extends AbstractComponent<AlarmControlPanel.ChannelConfiguration> {
|
||||
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<ComponentAlarm
|
|||
/**
|
||||
* Configuration class for MQTT component
|
||||
*/
|
||||
static class ChannelConfiguration extends BaseChannelConfiguration {
|
||||
static class ChannelConfiguration extends AbstractChannelConfiguration {
|
||||
ChannelConfiguration() {
|
||||
super("MQTT Alarm");
|
||||
}
|
||||
|
@ -55,30 +56,33 @@ public class ComponentAlarmControlPanel extends AbstractComponent<ComponentAlarm
|
|||
protected String payload_arm_away = "ARM_AWAY";
|
||||
}
|
||||
|
||||
public ComponentAlarmControlPanel(CFactory.ComponentConfiguration componentConfiguration) {
|
||||
public AlarmControlPanel(ComponentFactory.ComponentConfiguration componentConfiguration) {
|
||||
super(componentConfiguration, ChannelConfiguration.class);
|
||||
|
||||
final String[] state_enum = { channelConfiguration.state_disarmed, channelConfiguration.state_armed_home,
|
||||
channelConfiguration.state_armed_away, channelConfiguration.state_pending,
|
||||
channelConfiguration.state_triggered };
|
||||
buildChannel(stateChannelID, new TextValue(state_enum), channelConfiguration.name,
|
||||
buildChannel(stateChannelID, new TextValue(state_enum), channelConfiguration.getName(),
|
||||
componentConfiguration.getUpdateListener())
|
||||
.stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)//
|
||||
.stateTopic(channelConfiguration.state_topic, channelConfiguration.getValueTemplate())//
|
||||
.build();
|
||||
|
||||
String command_topic = channelConfiguration.command_topic;
|
||||
if (command_topic != null) {
|
||||
buildChannel(switchDisarmChannelID, new TextValue(new String[] { channelConfiguration.payload_disarm }),
|
||||
channelConfiguration.name, componentConfiguration.getUpdateListener())
|
||||
.commandTopic(command_topic, channelConfiguration.retain).build();
|
||||
channelConfiguration.getName(), componentConfiguration.getUpdateListener())
|
||||
.commandTopic(command_topic, channelConfiguration.isRetain(), channelConfiguration.getQos())
|
||||
.build();
|
||||
|
||||
buildChannel(switchArmHomeChannelID, new TextValue(new String[] { channelConfiguration.payload_arm_home }),
|
||||
channelConfiguration.name, componentConfiguration.getUpdateListener())
|
||||
.commandTopic(command_topic, channelConfiguration.retain).build();
|
||||
channelConfiguration.getName(), componentConfiguration.getUpdateListener())
|
||||
.commandTopic(command_topic, channelConfiguration.isRetain(), channelConfiguration.getQos())
|
||||
.build();
|
||||
|
||||
buildChannel(switchArmAwayChannelID, new TextValue(new String[] { channelConfiguration.payload_arm_away }),
|
||||
channelConfiguration.name, componentConfiguration.getUpdateListener())
|
||||
.commandTopic(command_topic, channelConfiguration.retain).build();
|
||||
channelConfiguration.getName(), componentConfiguration.getUpdateListener())
|
||||
.commandTopic(command_topic, channelConfiguration.isRetain(), channelConfiguration.getQos())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
@ -19,6 +19,7 @@ import org.eclipse.jdt.annotation.Nullable;
|
|||
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
|
||||
import org.openhab.binding.mqtt.generic.values.OnOffValue;
|
||||
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;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.listener.OffDelayUpdateStateListener;
|
||||
|
||||
|
@ -28,13 +29,13 @@ import org.openhab.binding.mqtt.homeassistant.internal.listener.OffDelayUpdateSt
|
|||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ComponentBinarySensor extends AbstractComponent<ComponentBinarySensor.ChannelConfiguration> {
|
||||
public class BinarySensor extends AbstractComponent<BinarySensor.ChannelConfiguration> {
|
||||
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 @@ public class ComponentBinarySensor extends AbstractComponent<ComponentBinarySens
|
|||
protected @Nullable List<String> 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();
|
||||
|
|
@ -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 @@ import org.openhab.binding.mqtt.generic.values.ImageValue;
|
|||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ComponentCamera extends AbstractComponent<ComponentCamera.ChannelConfiguration> {
|
||||
public class Camera extends AbstractComponent<Camera.ChannelConfiguration> {
|
||||
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 @@ public class ComponentCamera extends AbstractComponent<ComponentCamera.ChannelCo
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -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<Climate.ChannelConfiguration> {
|
||||
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<String> 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<String> 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<String> 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<String> 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<String> 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<Command> 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<Command> 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<Command> getCommandFilter(@Nullable ComponentChannel actionChannel) {
|
||||
if (actionChannel == null) {
|
||||
return null;
|
||||
}
|
||||
final var val = actionChannel.getState().getCache();
|
||||
return command -> !ACTION_OFF_STATE.equals(val.getChannelState());
|
||||
}
|
||||
}
|
|
@ -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.eclipse.jdt.annotation.Nullable;
|
|||
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 @@ import com.google.gson.Gson;
|
|||
* @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 @@ public class CFactory {
|
|||
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 class CFactory {
|
|||
return scheduler;
|
||||
}
|
||||
|
||||
public <C extends BaseChannelConfiguration> C getConfig(Class<C> clazz) {
|
||||
return BaseChannelConfiguration.fromString(configJSON, gson, clazz);
|
||||
public <C extends AbstractChannelConfiguration> C getConfig(Class<C> clazz) {
|
||||
return AbstractChannelConfiguration.fromString(configJSON, gson, clazz);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 @@ import org.openhab.binding.mqtt.generic.values.RollershutterValue;
|
|||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ComponentCover extends AbstractComponent<ComponentCover.ChannelConfiguration> {
|
||||
public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
|
||||
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 @@ public class ComponentCover extends AbstractComponent<ComponentCover.ChannelConf
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -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 @@ import org.openhab.binding.mqtt.generic.values.OnOffValue;
|
|||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ComponentFan extends AbstractComponent<ComponentFan.ChannelConfiguration> {
|
||||
public class Fan extends AbstractComponent<Fan.ChannelConfiguration> {
|
||||
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 @@ public class ComponentFan extends AbstractComponent<ComponentFan.ChannelConfigur
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -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.eclipse.jdt.annotation.Nullable;
|
|||
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 @@ import org.openhab.core.types.State;
|
|||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ComponentLight extends AbstractComponent<ComponentLight.ChannelConfiguration>
|
||||
implements ChannelStateUpdateListener {
|
||||
public class Light extends AbstractComponent<Light.ChannelConfiguration> 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<ComponentLight.ChannelConf
|
|||
/**
|
||||
* Configuration class for MQTT component
|
||||
*/
|
||||
static class ChannelConfiguration extends BaseChannelConfiguration {
|
||||
static class ChannelConfiguration extends AbstractChannelConfiguration {
|
||||
ChannelConfiguration() {
|
||||
super("MQTT Light");
|
||||
}
|
||||
|
@ -93,30 +94,36 @@ public class ComponentLight extends AbstractComponent<ComponentLight.ChannelConf
|
|||
protected String payload_off = "OFF";
|
||||
}
|
||||
|
||||
protected CChannel colorChannel;
|
||||
protected CChannel switchChannel;
|
||||
protected CChannel brightnessChannel;
|
||||
protected ComponentChannel colorChannel;
|
||||
protected ComponentChannel switchChannel;
|
||||
protected ComponentChannel brightnessChannel;
|
||||
private final @Nullable ChannelStateUpdateListener channelStateUpdateListener;
|
||||
|
||||
public ComponentLight(CFactory.ComponentConfiguration builder) {
|
||||
public Light(ComponentFactory.ComponentConfiguration builder) {
|
||||
super(builder, ChannelConfiguration.class);
|
||||
this.channelStateUpdateListener = builder.getUpdateListener();
|
||||
ColorValue value = new ColorValue(ColorMode.RGB, channelConfiguration.payload_on,
|
||||
channelConfiguration.payload_off, 100);
|
||||
|
||||
// Create three MQTT subscriptions and use this class object as update listener
|
||||
switchChannel = buildChannel(switchChannelID, value, channelConfiguration.name, this)
|
||||
switchChannel = buildChannel(switchChannelID, value, channelConfiguration.getName(), this)
|
||||
.stateTopic(channelConfiguration.state_topic, channelConfiguration.state_value_template,
|
||||
channelConfiguration.value_template)
|
||||
.commandTopic(channelConfiguration.command_topic, channelConfiguration.retain).build(false);
|
||||
channelConfiguration.getValueTemplate())
|
||||
.commandTopic(channelConfiguration.command_topic, channelConfiguration.isRetain(),
|
||||
channelConfiguration.getQos())
|
||||
.build(false);
|
||||
|
||||
colorChannel = buildChannel(colorChannelID, value, channelConfiguration.name, this)
|
||||
colorChannel = buildChannel(colorChannelID, value, channelConfiguration.getName(), this)
|
||||
.stateTopic(channelConfiguration.rgb_state_topic, channelConfiguration.rgb_value_template)
|
||||
.commandTopic(channelConfiguration.rgb_command_topic, channelConfiguration.retain).build(false);
|
||||
.commandTopic(channelConfiguration.rgb_command_topic, channelConfiguration.isRetain(),
|
||||
channelConfiguration.getQos())
|
||||
.build(false);
|
||||
|
||||
brightnessChannel = buildChannel(brightnessChannelID, value, channelConfiguration.name, this)
|
||||
brightnessChannel = buildChannel(brightnessChannelID, value, channelConfiguration.getName(), this)
|
||||
.stateTopic(channelConfiguration.brightness_state_topic, channelConfiguration.brightness_value_template)
|
||||
.commandTopic(channelConfiguration.brightness_command_topic, channelConfiguration.retain).build(false);
|
||||
.commandTopic(channelConfiguration.brightness_command_topic, channelConfiguration.isRetain(),
|
||||
channelConfiguration.getQos())
|
||||
.build(false);
|
||||
|
||||
channels.put(colorChannelID, colorChannel);
|
||||
}
|
|
@ -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 lock, following the https://www.home-assistant.io/components/lock.mqtt/ specification.
|
||||
|
@ -22,13 +23,13 @@ import org.openhab.binding.mqtt.generic.values.OnOffValue;
|
|||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ComponentLock extends AbstractComponent<ComponentLock.ChannelConfiguration> {
|
||||
public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
|
||||
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 @@ public class ComponentLock extends AbstractComponent<ComponentLock.ChannelConfig
|
|||
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 class ComponentLock extends AbstractComponent<ComponentLock.ChannelConfig
|
|||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -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.ChannelStateUpdateListener;
|
|||
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 @@ import org.openhab.binding.mqtt.homeassistant.internal.listener.ExpireUpdateStat
|
|||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ComponentSensor extends AbstractComponent<ComponentSensor.ChannelConfiguration> {
|
||||
public class Sensor extends AbstractComponent<Sensor.ChannelConfiguration> {
|
||||
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 @@ public class ComponentSensor extends AbstractComponent<ComponentSensor.ChannelCo
|
|||
protected @Nullable List<String> 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 class ComponentSensor extends AbstractComponent<ComponentSensor.ChannelCo
|
|||
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();
|
||||
|
|
@ -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 @@ import org.openhab.binding.mqtt.generic.values.OnOffValue;
|
|||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ComponentSwitch extends AbstractComponent<ComponentSwitch.ChannelConfiguration> {
|
||||
public class Switch extends AbstractComponent<Switch.ChannelConfiguration> {
|
||||
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 @@ public class ComponentSwitch extends AbstractComponent<ComponentSwitch.ChannelCo
|
|||
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 class ComponentSwitch extends AbstractComponent<ComponentSwitch.ChannelCo
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -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 @@ import com.google.gson.stream.JsonWriter;
|
|||
/**
|
||||
* This a Gson type adapter factory.
|
||||
*
|
||||
* It will create a type adapter for every class derived from {@link BaseChannelConfiguration} and ensures,
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* In elements, whose name end in'_topic' '~' replacement is performed.
|
||||
*
|
||||
* The adapters also handle {@link BaseChannelConfiguration.Device}
|
||||
* <p>
|
||||
* The adapters also handle {@link Device}
|
||||
*
|
||||
* @author Jochen Klein - Initial contribution
|
||||
*/
|
||||
|
@ -46,21 +53,22 @@ public class ChannelConfigurationTypeAdapterFactory implements TypeAdapterFactor
|
|||
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 <T> TypeAdapter<T> createHAConfig(Gson gson, TypeToken<T> type) {
|
||||
/* The delegate is the 'default' adapter */
|
||||
|
@ -72,7 +80,7 @@ public class ChannelConfigurationTypeAdapterFactory implements TypeAdapterFactor
|
|||
/* 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 class ChannelConfigurationTypeAdapterFactory implements TypeAdapterFactor
|
|||
};
|
||||
}
|
||||
|
||||
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 @@ public class ChannelConfigurationTypeAdapterFactory implements TypeAdapterFactor
|
|||
}
|
||||
|
||||
field.set(config, newValue);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
|
@ -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 @@ import com.google.gson.JsonParseException;
|
|||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
public class ConnectionDeserializer implements JsonDeserializer<BaseChannelConfiguration.Connection> {
|
||||
public class ConnectionDeserializer implements JsonDeserializer<Connection> {
|
||||
@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());
|
||||
}
|
||||
}
|
|
@ -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 class ListOrStringDeserializer extends TypeAdapter<List<String>> {
|
|||
in.nextNull();
|
||||
return null;
|
||||
case STRING:
|
||||
return Arrays.asList(in.nextString());
|
||||
return Collections.singletonList(in.nextString());
|
||||
case BEGIN_ARRAY:
|
||||
return readList(in);
|
||||
default:
|
|
@ -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.thing.Thing;
|
|||
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 @@ import com.google.gson.annotations.SerializedName;
|
|||
* @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 extends BaseChannelConfiguration> C fromString(final String configJSON, final Gson gson,
|
||||
final Class<C> 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,14 +39,34 @@ public abstract class BaseChannelConfiguration {
|
|||
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> availability;
|
||||
|
||||
@SerializedName(value = "~")
|
||||
protected String tilde = "";
|
||||
|
||||
protected BaseChannelConfiguration(String defaultName) {
|
||||
protected @Nullable Device device;
|
||||
|
||||
/**
|
||||
* 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 AbstractChannelConfiguration(String defaultName) {
|
||||
this.name = defaultName;
|
||||
}
|
||||
|
||||
|
@ -91,31 +74,7 @@ public abstract class BaseChannelConfiguration {
|
|||
return value == null ? null : value.replaceAll("~", tilde);
|
||||
}
|
||||
|
||||
protected @Nullable Device device;
|
||||
|
||||
static class Device {
|
||||
@JsonAdapter(ListOrStringDeserializer.class)
|
||||
protected @Nullable List<String> identifiers;
|
||||
protected @Nullable List<Connection> connections;
|
||||
protected @Nullable String manufacturer;
|
||||
protected @Nullable String model;
|
||||
protected @Nullable String name;
|
||||
protected @Nullable String sw_version;
|
||||
|
||||
public @Nullable String getId() {
|
||||
List<String> identifiers = this.identifiers;
|
||||
return identifiers == null ? null : String.join("_", identifiers);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonAdapter(ConnectionDeserializer.class)
|
||||
static class Connection {
|
||||
protected @Nullable String type;
|
||||
protected @Nullable String identifier;
|
||||
}
|
||||
|
||||
public String getThingName() {
|
||||
@Nullable
|
||||
String result = null;
|
||||
|
||||
if (this.device != null) {
|
||||
|
@ -128,7 +87,6 @@ public abstract class BaseChannelConfiguration {
|
|||
}
|
||||
|
||||
public String getThingId(String defaultId) {
|
||||
@Nullable
|
||||
String result = null;
|
||||
if (this.device != null) {
|
||||
result = this.device.getId();
|
||||
|
@ -152,10 +110,91 @@ public abstract class BaseChannelConfiguration {
|
|||
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<Availability> 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 extends AbstractChannelConfiguration> C fromString(final String configJSON, final Gson gson,
|
||||
final Class<C> clazz) {
|
||||
return Objects.requireNonNull(gson.fromJson(configJSON, clazz));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<String> identifiers;
|
||||
protected @Nullable List<Connection> 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<String> identifiers = this.identifiers;
|
||||
return identifiers == null ? null : String.join("_", identifiers);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<Connection> 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<String> getIdentifiers() {
|
||||
return identifiers;
|
||||
}
|
||||
}
|
|
@ -33,10 +33,10 @@ import org.openhab.binding.mqtt.discovery.AbstractMQTTDiscovery;
|
|||
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 class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
|
|||
}
|
||||
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.
|
||||
|
|
|
@ -32,14 +32,14 @@ import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
|
|||
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 class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||
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 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||
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 class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||
|
||||
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 class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||
// 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<Channel> channels = discovered.channelTypes().values().stream().map(CChannel::getChannel)
|
||||
.collect(Collectors.toList());
|
||||
Collection<Channel> channels = discovered.getChannelMap().values().stream()
|
||||
.map(ComponentChannel::getChannel).collect(Collectors.toList());
|
||||
ThingHelper.addChannelsToThing(thing, channels);
|
||||
}
|
||||
}
|
||||
|
@ -314,7 +314,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||
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());
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<String, Set<MqttMessageSubscriber>> 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";
|
||||
}
|
||||
}
|
|
@ -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")));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> getConfigTopics();
|
||||
|
||||
/**
|
||||
* Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
|
||||
*
|
||||
* @param mqttTopic mqtt topic with configuration
|
||||
* @param json configuration payload in Json
|
||||
* @return discovered component
|
||||
*/
|
||||
protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
|
||||
String json) {
|
||||
return discoverComponent(mqttTopic, json.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
|
||||
*
|
||||
* @param mqttTopic mqtt topic with configuration
|
||||
* @param jsonPayload configuration payload in Json
|
||||
* @return discovered component
|
||||
*/
|
||||
protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
|
||||
byte[] jsonPayload) {
|
||||
var latch = thingHandler.createWaitForComponentDiscoveredLatch(1);
|
||||
assertThat(publishMessage(mqttTopic, jsonPayload), is(true));
|
||||
try {
|
||||
assert latch.await(1, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
assertThat(e.getMessage(), false);
|
||||
}
|
||||
var component = thingHandler.getDiscoveredComponent();
|
||||
assertThat(component, CoreMatchers.notNullValue());
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert channel topics, label and value class
|
||||
*
|
||||
* @param component component
|
||||
* @param channelId channel
|
||||
* @param stateTopic state topic or empty string
|
||||
* @param commandTopic command topic or empty string
|
||||
* @param label label
|
||||
* @param valueClass value class
|
||||
*/
|
||||
protected static void assertChannel(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
|
||||
String channelId, String stateTopic, String commandTopic, String label, Class<? extends Value> valueClass) {
|
||||
var stateChannel = component.getChannel(channelId);
|
||||
assertChannel(stateChannel, stateTopic, commandTopic, label, valueClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert channel topics, label and value class
|
||||
*
|
||||
* @param stateChannel channel
|
||||
* @param stateTopic state topic or empty string
|
||||
* @param commandTopic command topic or empty string
|
||||
* @param label label
|
||||
* @param valueClass value class
|
||||
*/
|
||||
protected static void assertChannel(ComponentChannel stateChannel, String stateTopic, String commandTopic,
|
||||
String label, Class<? extends Value> valueClass) {
|
||||
assertThat(stateChannel.getChannel().getLabel(), is(label));
|
||||
assertThat(stateChannel.getState().getStateTopic(), is(stateTopic));
|
||||
assertThat(stateChannel.getState().getCommandTopic(), is(commandTopic));
|
||||
assertThat(stateChannel.getState().getCache(), is(instanceOf(valueClass)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert channel state
|
||||
*
|
||||
* @param component component
|
||||
* @param channelId channel
|
||||
* @param state expected state
|
||||
*/
|
||||
protected static void assertState(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
|
||||
String channelId, State state) {
|
||||
assertThat(component.getChannel(channelId).getState().getCache().getChannelState(), is(state));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that given payload was published exact-once on given topic.
|
||||
*
|
||||
* @param mqttTopic Mqtt topic
|
||||
* @param payload payload
|
||||
*/
|
||||
protected void assertPublished(String mqttTopic, String payload) {
|
||||
verify(bridgeConnection).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(),
|
||||
anyBoolean());
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that given payload was published N times on given topic.
|
||||
*
|
||||
* @param mqttTopic Mqtt topic
|
||||
* @param payload payload
|
||||
* @param t payload must be published N times on given topic
|
||||
*/
|
||||
protected void assertPublished(String mqttTopic, String payload, int t) {
|
||||
verify(bridgeConnection, times(t)).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)),
|
||||
anyInt(), anyBoolean());
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that given payload was not published on given topic.
|
||||
*
|
||||
* @param mqttTopic Mqtt topic
|
||||
* @param payload payload
|
||||
*/
|
||||
protected void assertNotPublished(String mqttTopic, String payload) {
|
||||
verify(bridgeConnection, never()).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(),
|
||||
anyBoolean());
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish payload to all subscribers on specified topic.
|
||||
*
|
||||
* @param mqttTopic Mqtt topic
|
||||
* @param payload payload
|
||||
* @return true when at least one subscriber found
|
||||
*/
|
||||
protected boolean publishMessage(String mqttTopic, String payload) {
|
||||
return publishMessage(mqttTopic, payload.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish payload to all subscribers on specified topic.
|
||||
*
|
||||
* @param mqttTopic Mqtt topic
|
||||
* @param payload payload
|
||||
* @return true when at least one subscriber found
|
||||
*/
|
||||
protected boolean publishMessage(String mqttTopic, byte[] payload) {
|
||||
final var topicSubscribers = subscriptions.get(mqttTopic);
|
||||
|
||||
if (topicSubscribers != null && !topicSubscribers.isEmpty()) {
|
||||
topicSubscribers.forEach(mqttMessageSubscriber -> mqttMessageSubscriber.processMessage(mqttTopic, payload));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNullByDefault
|
||||
protected static class LatchThingHandler extends HomeAssistantThingHandler {
|
||||
private @Nullable CountDownLatch latch;
|
||||
private @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoveredComponent;
|
||||
|
||||
public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
|
||||
TransformationServiceProvider transformationServiceProvider, int subscribeTimeout,
|
||||
int attributeReceiveTimeout) {
|
||||
super(thing, channelTypeProvider, transformationServiceProvider, subscribeTimeout, attributeReceiveTimeout);
|
||||
}
|
||||
|
||||
public void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<@NonNull ?> component) {
|
||||
accept(List.of(component));
|
||||
discoveredComponent = component;
|
||||
if (latch != null) {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
public CountDownLatch createWaitForComponentDiscoveredLatch(int count) {
|
||||
final var newLatch = new CountDownLatch(count);
|
||||
latch = newLatch;
|
||||
return newLatch;
|
||||
}
|
||||
|
||||
public @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> getDiscoveredComponent() {
|
||||
return discoveredComponent;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> getConfigTopics() {
|
||||
return Set.of(CONFIG_TOPIC);
|
||||
}
|
||||
}
|
|
@ -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<String> getConfigTopics() {
|
||||
return Set.of(CONFIG_TOPIC);
|
||||
}
|
||||
}
|
|
@ -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<String> getConfigTopics() {
|
||||
return Set.of(CONFIG_TOPIC);
|
||||
}
|
||||
}
|
|
@ -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<String> getConfigTopics() {
|
||||
return Set.of(CONFIG_TOPIC);
|
||||
}
|
||||
}
|
|
@ -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<String> getConfigTopics() {
|
||||
return Set.of(CONFIG_TOPIC);
|
||||
}
|
||||
}
|
|
@ -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<String> getConfigTopics() {
|
||||
return Set.of(CONFIG_TOPIC);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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<String> getConfigTopics() {
|
||||
return Set.of(CONFIG_TOPIC);
|
||||
}
|
||||
}
|
|
@ -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<String> getConfigTopics() {
|
||||
return Set.of(CONFIG_TOPIC);
|
||||
}
|
||||
}
|
|
@ -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<String> getConfigTopics() {
|
||||
return Set.of(CONFIG_TOPIC);
|
||||
}
|
||||
}
|
|
@ -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<String> getConfigTopics() {
|
||||
return Set.of(CONFIG_TOPIC);
|
||||
}
|
||||
}
|
|
@ -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<String>) 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<DiscoveryResult> 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<ThingUID> removeOlderResults(DiscoveryService source, long timestamp,
|
||||
@Nullable Collection<ThingTypeUID> thingTypeUIDs, @Nullable ThingUID bridgeUID) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public CopyOnWriteArrayList<DiscoveryResult> getDiscoveryResults() {
|
||||
return discoveryResults;
|
||||
}
|
||||
|
||||
public CountDownLatch createWaitForThingsDiscoveredLatch(int count) {
|
||||
final var newLatch = new CountDownLatch(count);
|
||||
latch = newLatch;
|
||||
return newLatch;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> 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<String> 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());
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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 }}"
|
||||
}
|
|
@ -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"
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
Loading…
Reference in New Issue