[mqtt.homeassistant] Use a single channel for all events from a single button (#17598)

Use the `subtype` field to collapse multiple DeviceAutomation components into
a single channel.

Signed-off-by: Cody Cutrer <cody@cutrer.us>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Cody Cutrer 2024-10-21 14:24:56 -05:00 committed by Ciprian Pascu
parent 41d19067bf
commit 89b4e2267d
10 changed files with 350 additions and 55 deletions

View File

@ -85,8 +85,12 @@ public class ComponentChannel {
channelState.setChannelUID(channelUID);
}
public void resetConfiguration(Configuration configuration) {
channel = ChannelBuilder.create(channel).withConfiguration(configuration).build();
}
public void clearConfiguration() {
channel = ChannelBuilder.create(channel).withConfiguration(new Configuration()).build();
resetConfiguration(new Configuration());
}
public ChannelState getState() {
@ -142,6 +146,8 @@ public class ComponentChannel {
private @Nullable String templateIn;
private @Nullable String templateOut;
private @Nullable Configuration configuration;
private String format = "%s";
public Builder(AbstractComponent<?> component, String channelID, ChannelTypeUID channelTypeUID,
@ -225,6 +231,11 @@ public class ComponentChannel {
return this;
}
public Builder withConfiguration(Configuration configuration) {
this.configuration = configuration;
return this;
}
// If the component explicitly specifies optimistic, or it's missing a state topic
// put it in optimistic mode (which, in openHAB parlance, means to auto-update the
// item).
@ -286,9 +297,12 @@ public class ComponentChannel {
commandDescription = valueState.createCommandDescription().build();
}
Configuration configuration = new Configuration();
configuration.put("config", component.getChannelConfigurationJson());
component.getHaID().toConfig(configuration);
Configuration configuration = this.configuration;
if (configuration == null) {
configuration = new Configuration();
configuration.put("config", component.getChannelConfigurationJson());
component.getHaID().toConfig(configuration);
}
channel = ChannelBuilder.create(channelUID, channelState.getItemType()).withType(channelTypeUID)
.withKind(kind).withLabel(label).withConfiguration(configuration)

View File

@ -91,11 +91,11 @@ public class HaID {
private static String createTopic(HaID id) {
StringBuilder str = new StringBuilder();
str.append(id.baseTopic).append('/').append(id.component).append('/');
str.append(id.component).append('/');
if (!id.nodeID.isBlank()) {
str.append(id.nodeID).append('/');
}
str.append(id.objectID).append('/');
str.append(id.objectID);
return str.toString();
}
@ -232,7 +232,7 @@ public class HaID {
* @return fallback group id
*/
public String getTopic(String suffix) {
return topic + suffix;
return baseTopic + "/" + topic + "/" + suffix;
}
@Override

View File

@ -12,12 +12,18 @@
*/
package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.core.config.core.Configuration;
import com.google.gson.annotations.SerializedName;
@ -31,7 +37,7 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends AbstractChannelConfiguration {
public static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Device Trigger");
}
@ -43,6 +49,14 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
protected String subtype = "";
protected @Nullable String payload;
public String getTopic() {
return topic;
}
public String getSubtype() {
return subtype;
}
}
public DeviceTrigger(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
@ -58,6 +72,14 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
throw new ConfigurationException("Component:DeviceTrigger must have a subtype");
}
if (newStyleChannels) {
// Name the channel after the subtype, not the component ID
// So that we only end up with a single channel for all possible events
// for a single button (subtype is the button, type is type of press)
componentId = channelConfiguration.subtype;
groupId = null;
}
TextValue value;
String payload = channelConfiguration.payload;
if (payload != null) {
@ -69,6 +91,54 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
buildChannel(channelConfiguration.type, ComponentChannelType.TRIGGER, value, getName(),
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.topic, channelConfiguration.getValueTemplate()).trigger(true).build();
finalizeChannels();
}
/**
* Take another DeviceTrigger (presumably whose subtype, topic, and value template match),
* and adjust this component's channel to accept the payload that trigger allows.
*
* @return if the component was stopped, and thus needs restarted
*/
public boolean merge(DeviceTrigger other) {
ComponentChannel channel = channels.get(componentId);
TextValue value = (TextValue) channel.getState().getCache();
Set<String> payloads = value.getStates();
// Append objectid/config to channel configuration
Configuration currentConfiguration = channel.getChannel().getConfiguration();
Configuration newConfiguration = new Configuration();
newConfiguration.put("component", currentConfiguration.get("component"));
newConfiguration.put("nodeid", currentConfiguration.get("nodeid"));
Object objectIdObject = currentConfiguration.get("objectid");
if (objectIdObject instanceof String objectIdString) {
newConfiguration.put("objectid", List.of(objectIdString, other.getHaID().objectID));
} else if (objectIdObject instanceof List<?> objectIdList) {
newConfiguration.put("objectid",
Stream.concat(objectIdList.stream(), Stream.of(other.getHaID().objectID)).toList());
}
Object configObject = currentConfiguration.get("config");
if (configObject instanceof String configString) {
newConfiguration.put("config", List.of(configString, other.getChannelConfigurationJson()));
} else if (configObject instanceof List<?> configList) {
newConfiguration.put("config",
Stream.concat(configList.stream(), Stream.of(other.getChannelConfigurationJson())).toList());
}
// Append payload to allowed values
String otherPayload = other.getChannelConfiguration().payload;
if (payloads == null || otherPayload == null) {
// Need to accept anything
value = new TextValue();
} else {
String[] newValues = Stream.concat(payloads.stream(), Stream.of(otherPayload)).toArray(String[]::new);
value = new TextValue(newValues);
}
// Recreate the channel
stop();
buildChannel(channelConfiguration.type, ComponentChannelType.TRIGGER, value, componentId,
componentConfiguration.getUpdateListener()).withConfiguration(newConfiguration)
.stateTopic(channelConfiguration.topic, channelConfiguration.getValueTemplate()).trigger(true).build();
return true;
}
}

View File

@ -13,9 +13,11 @@
package org.openhab.binding.mqtt.homeassistant.internal.handler;
import java.net.URI;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -39,6 +41,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.HaID;
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory;
import org.openhab.binding.mqtt.homeassistant.internal.component.DeviceTrigger;
import org.openhab.binding.mqtt.homeassistant.internal.component.Update;
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
@ -156,35 +159,39 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
continue;
}
}
Configuration channelConfig = channel.getConfiguration();
if (!channelConfig.containsKey("component")
|| !channelConfig.containsKey("objectid") | !channelConfig.containsKey("config")) {
Configuration multiComponentChannelConfig = channel.getConfiguration();
if (!multiComponentChannelConfig.containsKey("component")
|| !multiComponentChannelConfig.containsKey("objectid")
|| !multiComponentChannelConfig.containsKey("config")) {
// Must be a secondary channel
continue;
}
HaID haID = HaID.fromConfig(config.basetopic, channelConfig);
List<Configuration> flattenedConfig = flattenChannelConfiguration(multiComponentChannelConfig,
channel.getUID());
for (Configuration channelConfig : flattenedConfig) {
HaID haID = HaID.fromConfig(config.basetopic, channelConfig);
if (!config.topics.contains(haID.getTopic())) {
// don't add a component for this channel that isn't configured on the thing
// anymore
// It will disappear from the thing when the thing type is updated below
continue;
}
discoveryHomeAssistantIDs.add(haID);
ThingUID thingUID = channel.getUID().getThingUID();
String channelConfigurationJSON = (String) channelConfig.get("config");
try {
AbstractComponent<?> component = ComponentFactory.createComponent(thingUID, haID,
channelConfigurationJSON, this, this, scheduler, gson, jinjava, newStyleChannels);
if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
typeID = calculateThingTypeUID(component);
if (!config.topics.contains(haID.getTopic())) {
// don't add a component for this channel that isn't configured on the thing
// anymore. It will disappear from the thing when the thing type is updated below
continue;
}
addComponent(component);
} catch (ConfigurationException e) {
logger.warn("Cannot restore component {}: {}", thing, e.getMessage());
discoveryHomeAssistantIDs.add(haID);
ThingUID thingUID = channel.getUID().getThingUID();
String channelConfigurationJSON = (String) channelConfig.get("config");
try {
AbstractComponent<?> component = ComponentFactory.createComponent(thingUID, haID,
channelConfigurationJSON, this, this, scheduler, gson, jinjava, newStyleChannels);
if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
typeID = calculateThingTypeUID(component);
}
addComponent(component);
} catch (ConfigurationException e) {
logger.warn("Cannot restore component {}: {}", thing, e.getMessage());
}
}
}
if (updateThingType(typeID)) {
@ -280,7 +287,8 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
AbstractComponent<?> known = haComponentsByUniqueId.get(discovered.getUniqueId());
// Is component already known?
if (known != null) {
if (discovered.getConfigHash() != known.getConfigHash()) {
if (discovered.getConfigHash() != known.getConfigHash()
&& discovered.getUniqueId().equals(known.getUniqueId())) {
// Don't wait for the future to complete. We are also not interested in failures.
// The component will be replaced in a moment.
known.stop();
@ -422,6 +430,35 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
private void addComponent(AbstractComponent component) {
AbstractComponent existing = haComponents.get(component.getComponentId());
if (existing != null) {
// DeviceTriggers that are for the same subtype, topic, and value template
// can be coalesced together
if (component instanceof DeviceTrigger newTrigger && existing instanceof DeviceTrigger oldTrigger
&& newTrigger.getChannelConfiguration().getSubtype()
.equals(oldTrigger.getChannelConfiguration().getSubtype())
&& newTrigger.getChannelConfiguration().getTopic()
.equals(oldTrigger.getChannelConfiguration().getTopic())
&& oldTrigger.getHaID().nodeID.equals(newTrigger.getHaID().nodeID)) {
String newTriggerValueTemplate = newTrigger.getChannelConfiguration().getValueTemplate();
String oldTriggerValueTemplate = oldTrigger.getChannelConfiguration().getValueTemplate();
if ((newTriggerValueTemplate == null && oldTriggerValueTemplate == null)
|| (newTriggerValueTemplate != null & oldTriggerValueTemplate != null
&& newTriggerValueTemplate.equals(oldTriggerValueTemplate))) {
// Adjust the set of valid values
MqttBrokerConnection connection = this.connection;
if (oldTrigger.merge(newTrigger) && connection != null) {
// Make sure to re-start if this did something, and it was stopped
oldTrigger.start(connection, scheduler, 0).exceptionally(e -> {
logger.warn("Failed to start component {}", oldTrigger.getHaID(), e);
return null;
});
}
haComponentsByUniqueId.put(component.getUniqueId(), component);
System.out.println("don't forget to add to the channel config");
return;
}
}
// rename the conflict
haComponents.remove(existing.getComponentId());
existing.resolveConflict();
@ -431,4 +468,41 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
haComponents.put(component.getComponentId(), component);
haComponentsByUniqueId.put(component.getUniqueId(), component);
}
/**
* Takes a Configuration where objectid and config are a list, and generates
* multiple Configurations where there are single objects
*/
private List<Configuration> flattenChannelConfiguration(Configuration multiComponentChannelConfig,
ChannelUID channelUID) {
Object component = multiComponentChannelConfig.get("component");
Object nodeid = multiComponentChannelConfig.get("nodeid");
if ((multiComponentChannelConfig.get("objectid") instanceof List objectIds)
&& (multiComponentChannelConfig.get("config") instanceof List configurations)) {
if (objectIds.size() != configurations.size()) {
logger.warn("objectid and config for channel {} do not have the same number of items; ignoring",
channelUID);
return List.of();
}
List<Configuration> result = new ArrayList();
Iterator<Object> objectIdIterator = objectIds.iterator();
Iterator<Object> configIterator = configurations.iterator();
while (objectIdIterator.hasNext()) {
Configuration componentConfiguration = new Configuration();
componentConfiguration.put("component", component);
componentConfiguration.put("nodeid", nodeid);
componentConfiguration.put("objectid", objectIdIterator.next());
componentConfiguration.put("config", configIterator.next());
result.add(componentConfiguration);
}
return result;
} else {
return List.of(multiComponentChannelConfig);
}
}
// For testing
Map<@Nullable String, AbstractComponent<?>> getComponents() {
return haComponents;
}
}

View File

@ -15,12 +15,12 @@
<description>Optional node name of the component</description>
<default></default>
</parameter>
<parameter name="objectid" type="text" readOnly="true">
<parameter name="objectid" type="text" readOnly="true" multiple="true">
<label>Object ID</label>
<description>Object ID of the component</description>
<default></default>
</parameter>
<parameter name="config" type="text" readOnly="true">
<parameter name="config" type="text" readOnly="true" multiple="true">
<label>JSON Configuration</label>
<description>The JSON configuration string received by the component via MQTT.</description>
<default></default>

View File

@ -94,7 +94,7 @@ public abstract class AbstractHomeAssistantTests extends JavaTest {
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 Thing haThing = ThingBuilder.create(HA_TYPE_UID, HA_UID).withBridge(BRIDGE_UID).build();
protected final ConcurrentMap<String, Set<MqttMessageSubscriber>> subscriptions = new ConcurrentHashMap<>();
private @Mock @NonNullByDefault({}) TransformationService transformationService1Mock;

View File

@ -36,6 +36,7 @@ public class HaIDTests {
assertThat(subject.objectID, is("name"));
assertThat(subject.component, is("switch"));
assertThat(subject.getTopic(), is("switch/name"));
assertThat(subject.getTopic("suffix"), is("homeassistant/switch/name/suffix"));
Configuration config = new Configuration();
@ -58,6 +59,7 @@ public class HaIDTests {
assertThat(subject.objectID, is("name"));
assertThat(subject.component, is("switch"));
assertThat(subject.getTopic(), is("switch/node/name"));
assertThat(subject.getTopic("suffix"), is("homeassistant/switch/node/name/suffix"));
Configuration config = new Configuration();

View File

@ -80,6 +80,9 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
if (useNewStyleChannels()) {
haThing.setProperty("newStyleChannels", "true");
}
thingHandler = new LatchThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
@ -104,6 +107,13 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
*/
protected abstract Set<String> getConfigTopics();
/**
* If new style channels should be used for this test.
*/
protected boolean useNewStyleChannels() {
return false;
}
/**
* Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
*

View File

@ -14,12 +14,18 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.core.config.core.Configuration;
/**
* Tests for {@link DeviceTrigger}
@ -28,12 +34,13 @@ import org.openhab.binding.mqtt.generic.values.TextValue;
*/
@NonNullByDefault
public class DeviceTriggerTests extends AbstractComponentTests {
public static final String CONFIG_TOPIC = "device_automation/0x8cf681fffe2fd2a6";
public static final String CONFIG_TOPIC_1 = "device_automation/0x8cf681fffe2fd2a6/press";
public static final String CONFIG_TOPIC_2 = "device_automation/0x8cf681fffe2fd2a6/release";
@SuppressWarnings("null")
@Test
public void test() throws InterruptedException {
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC_1), """
{
"automation_type": "trigger",
"device": {
@ -56,19 +63,93 @@ public class DeviceTriggerTests extends AbstractComponentTests {
assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("MQTT Device Trigger"));
assertChannel(component, "action", "zigbee2mqtt/Charge Now Button/action", "", "MQTT Device Trigger",
assertChannel(component, "on", "zigbee2mqtt/Charge Now Button/action", "", "MQTT Device Trigger",
TextValue.class);
spyOnChannelUpdates(component, "action");
spyOnChannelUpdates(component, "on");
publishMessage("zigbee2mqtt/Charge Now Button/action", "on");
assertTriggered(component, "action", "on");
assertTriggered(component, "on", "on");
publishMessage("zigbee2mqtt/Charge Now Button/action", "off");
assertNotTriggered(component, "action", "off");
assertNotTriggered(component, "on", "off");
}
@SuppressWarnings("null")
@Test
public void testMerge() throws InterruptedException {
var component1 = (DeviceTrigger) discoverComponent(configTopicToMqtt(CONFIG_TOPIC_1), """
{
"automation_type": "trigger",
"device": {
"configuration_url": "#/device/0x8cf681fffe2fd2a6/info",
"identifiers": [
"zigbee2mqtt_0x8cf681fffe2fd2a6"
],
"manufacturer": "IKEA",
"model": "TRADFRI shortcut button (E1812)",
"name": "Charge Now Button",
"sw_version": "2.3.015"
},
"payload": "press",
"subtype": "turn_on",
"topic": "zigbee2mqtt/Charge Now Button/action",
"type": "button_long_press"
}
""");
var component2 = (DeviceTrigger) discoverComponent(configTopicToMqtt(CONFIG_TOPIC_2), """
{
"automation_type": "trigger",
"device": {
"configuration_url": "#/device/0x8cf681fffe2fd2a6/info",
"identifiers": [
"zigbee2mqtt_0x8cf681fffe2fd2a6"
],
"manufacturer": "IKEA",
"model": "TRADFRI shortcut button (E1812)",
"name": "Charge Now Button",
"sw_version": "2.3.015"
},
"payload": "release",
"subtype": "turn_on",
"topic": "zigbee2mqtt/Charge Now Button/action",
"type": "button_long_release"
}
""");
assertThat(component1.channels.size(), is(1));
ComponentChannel channel = Objects.requireNonNull(component1.getChannel("turn_on"));
TextValue value = (TextValue) channel.getState().getCache();
Set<String> payloads = value.getStates();
assertNotNull(payloads);
assertThat(payloads.size(), is(2));
assertThat(payloads.contains("press"), is(true));
assertThat(payloads.contains("release"), is(true));
Configuration channelConfig = channel.getChannel().getConfiguration();
Object config = channelConfig.get("config");
assertNotNull(config);
assertThat(config.getClass(), is(ArrayList.class));
List<?> configList = (List<?>) config;
assertThat(configList.size(), is(2));
spyOnChannelUpdates(component1, "turn_on");
publishMessage("zigbee2mqtt/Charge Now Button/action", "press");
assertTriggered(component1, "turn_on", "press");
publishMessage("zigbee2mqtt/Charge Now Button/action", "release");
assertTriggered(component1, "turn_on", "release");
publishMessage("zigbee2mqtt/Charge Now Button/action", "otherwise");
assertNotTriggered(component1, "turn_on", "otherwise");
}
@Override
protected Set<String> getConfigTopics() {
return Set.of(CONFIG_TOPIC);
return Set.of(CONFIG_TOPIC_1, CONFIG_TOPIC_2);
}
@Override
protected boolean useNewStyleChannels() {
return true;
}
}

View File

@ -12,6 +12,8 @@
*/
package org.openhab.binding.mqtt.homeassistant.internal.handler;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ -23,20 +25,25 @@ import java.util.Objects;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.ComponentChannelType;
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.Sensor;
import org.openhab.binding.mqtt.homeassistant.internal.component.Switch;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.StateDescription;
import com.hubspot.jinjava.Jinjava;
@ -76,6 +83,10 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
setupThingHandler();
}
protected void setupThingHandler() {
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, new Jinjava(), SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
@ -100,7 +111,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
});
verify(thingHandler, never()).componentDiscovered(any(), any());
assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
assertThat(haThing.getChannels().size(), is(0));
// Components discovered after messages in corresponding topics
var configTopic = "homeassistant/climate/0x847127fffe11dd6a_climate_zigbee2mqtt/config";
thingHandler.discoverComponents.processMessage(configTopic,
@ -108,7 +119,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopic)), any(Climate.class));
thingHandler.delayedProcessing.forceProcessNow();
assertThat(nonSpyThingHandler.getThing().getChannels().size(), CoreMatchers.is(6));
assertThat(nonSpyThingHandler.getThing().getChannels().size(), is(6));
verify(stateDescriptionProvider, times(6)).setDescription(any(), any(StateDescription.class));
verify(channelTypeProvider, times(1)).putChannelGroupType(any());
@ -119,7 +130,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopic)), any(Switch.class));
thingHandler.delayedProcessing.forceProcessNow();
assertThat(nonSpyThingHandler.getThing().getChannels().size(), CoreMatchers.is(7));
assertThat(nonSpyThingHandler.getThing().getChannels().size(), is(7));
verify(stateDescriptionProvider, atLeast(7)).setDescription(any(), any(StateDescription.class));
verify(channelTypeProvider, times(3)).putChannelGroupType(any());
}
@ -144,7 +155,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
});
verify(thingHandler, never()).componentDiscovered(any(), any());
assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
assertThat(haThing.getChannels().size(), is(0));
//
//
@ -211,13 +222,13 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
// verify that both channels are there and the label corresponds to newer discovery topic payload
//
Channel corridorTempChannel = nonSpyThingHandler.getThing().getChannel("tempCorridor_5Fsensor#sensor");
assertThat("Corridor temperature channel is created", corridorTempChannel, CoreMatchers.notNullValue());
assertThat("Corridor temperature channel is created", corridorTempChannel, notNullValue());
Objects.requireNonNull(corridorTempChannel); // for compiler
assertThat("Corridor temperature channel is having the updated label from 2nd discovery topic publish",
corridorTempChannel.getLabel(), CoreMatchers.is("CorridorTemp NEW"));
corridorTempChannel.getLabel(), is("CorridorTemp NEW"));
Channel outsideTempChannel = nonSpyThingHandler.getThing().getChannel("tempOutside_5Fsensor#sensor");
assertThat("Outside temperature channel is created", outsideTempChannel, CoreMatchers.notNullValue());
assertThat("Outside temperature channel is created", outsideTempChannel, notNullValue());
verify(thingHandler, times(2)).componentDiscovered(eq(new HaID(configTopicTempCorridor)), any(Sensor.class));
@ -242,7 +253,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
"homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config",
getResourceAsByteArray("component/configTS0601AutoLock.json"));
thingHandler.delayedProcessing.forceProcessNow();
assertThat(nonSpyThingHandler.getThing().getChannels().size(), CoreMatchers.is(7));
assertThat(nonSpyThingHandler.getThing().getChannels().size(), is(7));
verify(stateDescriptionProvider, atLeast(7)).setDescription(any(), any(StateDescription.class));
// When dispose
@ -270,7 +281,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
"homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config",
getResourceAsByteArray("component/configTS0601AutoLock.json"));
thingHandler.delayedProcessing.forceProcessNow();
assertThat(nonSpyThingHandler.getThing().getChannels().size(), CoreMatchers.is(7));
assertThat(nonSpyThingHandler.getThing().getChannels().size(), is(7));
// When dispose
nonSpyThingHandler.handleRemoval();
@ -288,7 +299,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
"{}".getBytes(StandardCharsets.UTF_8));
// Ignore unsupported component
thingHandler.delayedProcessing.forceProcessNow();
assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
assertThat(haThing.getChannels().size(), is(0));
}
@Test
@ -298,7 +309,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
"".getBytes(StandardCharsets.UTF_8));
// Ignore component with empty config
thingHandler.delayedProcessing.forceProcessNow();
assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
assertThat(haThing.getChannels().size(), is(0));
}
@Test
@ -308,6 +319,39 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
"{bad format}}".getBytes(StandardCharsets.UTF_8));
// Ignore component with bad format config
thingHandler.delayedProcessing.forceProcessNow();
assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
assertThat(haThing.getChannels().size(), is(0));
}
@Test
public void testRestoreComponentFromChannelConfig() {
Configuration thingConfiguration = new Configuration();
thingConfiguration.put("topics", List.of("switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/switch"));
Configuration channelConfiguration = new Configuration();
channelConfiguration.put("component", "switch");
channelConfiguration.put("objectid", List.of("switch"));
channelConfiguration.put("nodeid", "0x847127fffe11dd6a_auto_lock_zigbee2mqtt");
channelConfiguration.put("config", List.of("""
{
"command_topic": "zigbee2mqtt/th1/set/auto_lock",
"name": "th1 auto lock",
"state_topic": "zigbee2mqtt/th1",
"unique_id": "0x847127fffe11dd6a_auto_lock_zigbee2mqtt"
}
"""));
ChannelBuilder channelBuilder = ChannelBuilder
.create(new ChannelUID(haThing.getUID(), "switch"), CoreItemFactory.SWITCH)
.withType(ComponentChannelType.SWITCH.getChannelTypeUID()).withConfiguration(channelConfiguration);
haThing = ThingBuilder.create(HA_TYPE_UID, HA_UID).withBridge(BRIDGE_UID).withChannel(channelBuilder.build())
.withConfiguration(thingConfiguration).build();
haThing.setProperty("newStyleChannels", "true");
setupThingHandler();
thingHandler.initialize();
assertThat(thingHandler.getComponents().size(), is(1));
assertThat(thingHandler.getComponents().keySet().iterator().next(), is("switch"));
assertThat(thingHandler.getComponents().values().iterator().next().getClass(), is(Switch.class));
}
}