diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java index 1be08c07bea..d10ee4168a4 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java @@ -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) diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HaID.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HaID.java index cd61ed8a851..7abaf4bcb15 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HaID.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HaID.java @@ -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 diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTrigger.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTrigger.java index bd6beb40753..d3e1ebcc047 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTrigger.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTrigger.java @@ -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 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; } } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java index 1143a8e6690..7609fa7a44d 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java @@ -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 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 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 result = new ArrayList(); + Iterator objectIdIterator = objectIds.iterator(); + Iterator 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; + } } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/config/homeassistant-channel-config.xml b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/config/homeassistant-channel-config.xml index d3092a82b11..864e5afd08c 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/config/homeassistant-channel-config.xml +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/config/homeassistant-channel-config.xml @@ -15,12 +15,12 @@ Optional node name of the component - + Object ID of the component - + The JSON configuration string received by the component via MQTT. diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java index 4ef9d6e2668..645411963f5 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java @@ -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> subscriptions = new ConcurrentHashMap<>(); private @Mock @NonNullByDefault({}) TransformationService transformationService1Mock; diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HaIDTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HaIDTests.java index 3265a40b6b0..dbf5d2be45e 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HaIDTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HaIDTests.java @@ -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(); diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java index 4b090b6385d..2771859417b 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java @@ -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 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()} * diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTriggerTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTriggerTests.java index baa476704eb..96becd8e219 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTriggerTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTriggerTests.java @@ -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 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 getConfigTopics() { - return Set.of(CONFIG_TOPIC); + return Set.of(CONFIG_TOPIC_1, CONFIG_TOPIC_2); + } + + @Override + protected boolean useNewStyleChannels() { + return true; } } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java index ab469881547..26932da0396 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java @@ -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)); } }