[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>
This commit is contained in:
antroids
2021-08-15 12:48:26 +03:00
committed by GitHub
parent 9f09db1f18
commit 3a7835e122
56 changed files with 3236 additions and 466 deletions

View File

@@ -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";
}
}

View File

@@ -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")));
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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());
}
}

View File

@@ -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"
}

View File

@@ -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 }}"
}

View File

@@ -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"
}