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() {
private @Nullable String templateIn;
private @Nullable String templateOut;
+ private @Nullable Configuration configuration;
+
private String format = "%s";
public Builder(AbstractComponent<?> component, String channelID, ChannelTypeUID channelTypeUID,
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).
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)
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();
}
* @return fallback group id
*/
public String getTopic(String suffix) {
- return topic + suffix;
+ return baseTopic + "/" + topic + "/" + suffix;
}
@Override
*/
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;
/**
* Configuration class for MQTT component
*/
- static class ChannelConfiguration extends AbstractChannelConfiguration {
+ public static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Device Trigger");
}
protected String subtype = "";
protected @Nullable String payload;
+
+ public String getTopic() {
+ return topic;
+ }
+
+ public String getSubtype() {
+ return subtype;
+ }
}
public DeviceTrigger(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
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) {
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;
}
}
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;
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;
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)) {
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();
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();
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;
+ }
}
<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>
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;
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();
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();
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);
*/
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()}
*
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}
*/
@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": {
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;
}
}
*/
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.*;
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;
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);
});
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,
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());
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());
}
});
verify(thingHandler, never()).componentDiscovered(any(), any());
- assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
+ assertThat(haThing.getChannels().size(), is(0));
//
//
// 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));
"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
"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();
"{}".getBytes(StandardCharsets.UTF_8));
// Ignore unsupported component
thingHandler.delayedProcessing.forceProcessNow();
- assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
+ assertThat(haThing.getChannels().size(), is(0));
}
@Test
"".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
"{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));
}
}