[mqtt.homeassistant] Use a single channel for all scenes on a device (#18262)
It accepts either object ID, or scene name (assuming the latter doesn't conflict with the former). Signed-off-by: Cody Cutrer <cody@cutrer.us>pull/17817/merge
parent
a41c6d4b4f
commit
1856aa9b3c
|
@ -43,6 +43,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChanne
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Availability;
|
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Availability;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AvailabilityMode;
|
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AvailabilityMode;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device;
|
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device;
|
||||||
|
import org.openhab.core.config.core.Configuration;
|
||||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||||
import org.openhab.core.library.unit.ImperialUnits;
|
import org.openhab.core.library.unit.ImperialUnits;
|
||||||
import org.openhab.core.library.unit.SIUnits;
|
import org.openhab.core.library.unit.SIUnits;
|
||||||
|
@ -416,4 +417,45 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
|
||||||
private ChannelGroupTypeUID getChannelGroupTypeUID(String prefix) {
|
private ChannelGroupTypeUID getChannelGroupTypeUID(String prefix) {
|
||||||
return new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, prefix + "_" + uniqueId);
|
return new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, prefix + "_" + uniqueId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean mergeable(AbstractComponent<?> other) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Configuration mergeChannelConfiguration(ComponentChannel channel, AbstractComponent<C> other) {
|
||||||
|
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) {
|
||||||
|
if (!objectIdString.equals(other.getHaID().objectID)) {
|
||||||
|
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))
|
||||||
|
.sorted().distinct().toList());
|
||||||
|
}
|
||||||
|
Object configObject = currentConfiguration.get("config");
|
||||||
|
if (configObject instanceof String configString) {
|
||||||
|
if (!configString.equals(other.getChannelConfigurationJson())) {
|
||||||
|
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())).sorted()
|
||||||
|
.distinct().toList());
|
||||||
|
}
|
||||||
|
return newConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take another component of the same type, and merge it so that only one (set of)
|
||||||
|
* channel(s) exist on the Thing.
|
||||||
|
*
|
||||||
|
* @return if the component was stopped, and thus needs restarted
|
||||||
|
*/
|
||||||
|
public boolean merge(AbstractComponent<?> other) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
*/
|
*/
|
||||||
package org.openhab.binding.mqtt.homeassistant.internal.component;
|
package org.openhab.binding.mqtt.homeassistant.internal.component;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@ -91,44 +91,40 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
|
||||||
.stateTopic(channelConfiguration.topic, channelConfiguration.getValueTemplate()).trigger(true).build();
|
.stateTopic(channelConfiguration.topic, channelConfiguration.getValueTemplate()).trigger(true).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean mergeable(AbstractComponent<?> other) {
|
||||||
|
if (other instanceof DeviceTrigger newTrigger
|
||||||
|
&& newTrigger.getChannelConfiguration().getSubtype().equals(getChannelConfiguration().getSubtype())
|
||||||
|
&& newTrigger.getChannelConfiguration().getTopic().equals(getChannelConfiguration().getTopic())
|
||||||
|
&& getHaID().nodeID.equals(newTrigger.getHaID().nodeID)) {
|
||||||
|
String newTriggerValueTemplate = newTrigger.getChannelConfiguration().getValueTemplate();
|
||||||
|
String oldTriggerValueTemplate = getChannelConfiguration().getValueTemplate();
|
||||||
|
if ((newTriggerValueTemplate == null && oldTriggerValueTemplate == null)
|
||||||
|
|| (newTriggerValueTemplate != null & oldTriggerValueTemplate != null
|
||||||
|
&& newTriggerValueTemplate.equals(oldTriggerValueTemplate))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Take another DeviceTrigger (presumably whose subtype, topic, and value template match),
|
* Take another DeviceTrigger (presumably whose subtype, topic, and value template match),
|
||||||
* and adjust this component's channel to accept the payload that trigger allows.
|
* and adjust this component's channel to accept the payload that trigger allows.
|
||||||
*
|
*
|
||||||
* @return if the component was stopped, and thus needs restarted
|
* @return if the component was stopped, and thus needs restarted
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean merge(AbstractComponent<?> other) {
|
||||||
|
DeviceTrigger newTrigger = (DeviceTrigger) other;
|
||||||
|
ComponentChannel channel = Objects.requireNonNull(channels.get(componentId));
|
||||||
|
Configuration newConfiguration = mergeChannelConfiguration(channel, newTrigger);
|
||||||
|
|
||||||
public boolean merge(DeviceTrigger other) {
|
|
||||||
ComponentChannel channel = channels.get(componentId);
|
|
||||||
TextValue value = (TextValue) channel.getState().getCache();
|
TextValue value = (TextValue) channel.getState().getCache();
|
||||||
Set<String> payloads = value.getStates();
|
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) {
|
|
||||||
if (!objectIdString.equals(other.getHaID().objectID)) {
|
|
||||||
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))
|
|
||||||
.sorted().distinct().toList());
|
|
||||||
}
|
|
||||||
Object configObject = currentConfiguration.get("config");
|
|
||||||
if (configObject instanceof String configString) {
|
|
||||||
if (!configString.equals(other.getChannelConfigurationJson())) {
|
|
||||||
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())).sorted()
|
|
||||||
.distinct().toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append payload to allowed values
|
// Append payload to allowed values
|
||||||
String otherPayload = other.getChannelConfiguration().payload;
|
String otherPayload = newTrigger.getChannelConfiguration().payload;
|
||||||
if (payloads == null || otherPayload == null) {
|
if (payloads == null || otherPayload == null) {
|
||||||
// Need to accept anything
|
// Need to accept anything
|
||||||
value = new TextValue();
|
value = new TextValue();
|
||||||
|
|
|
@ -12,12 +12,24 @@
|
||||||
*/
|
*/
|
||||||
package org.openhab.binding.mqtt.homeassistant.internal.component;
|
package org.openhab.binding.mqtt.homeassistant.internal.component;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.openhab.binding.mqtt.generic.ChannelState;
|
||||||
import org.openhab.binding.mqtt.generic.values.TextValue;
|
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.ComponentChannelType;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
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 org.openhab.core.library.types.StringType;
|
||||||
import org.openhab.core.thing.type.AutoUpdatePolicy;
|
import org.openhab.core.thing.type.AutoUpdatePolicy;
|
||||||
|
import org.openhab.core.types.Command;
|
||||||
|
import org.openhab.core.types.CommandDescriptionBuilder;
|
||||||
|
import org.openhab.core.types.CommandOption;
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
@ -30,6 +42,29 @@ import com.google.gson.annotations.SerializedName;
|
||||||
public class Scene extends AbstractComponent<Scene.ChannelConfiguration> {
|
public class Scene extends AbstractComponent<Scene.ChannelConfiguration> {
|
||||||
public static final String SCENE_CHANNEL_ID = "scene";
|
public static final String SCENE_CHANNEL_ID = "scene";
|
||||||
|
|
||||||
|
// A command that has already been processed and routed to the correct Value,
|
||||||
|
// and should be immediately published. This will be the payloadOn value from
|
||||||
|
// the configuration
|
||||||
|
private static class SceneCommand extends StringType {
|
||||||
|
SceneCommand(String value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A value that can provide a proper CommandDescription with values and labels
|
||||||
|
class SceneValue extends TextValue {
|
||||||
|
SceneValue() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandDescriptionBuilder createCommandDescription() {
|
||||||
|
CommandDescriptionBuilder builder = super.createCommandDescription();
|
||||||
|
objectIdToScene.forEach((k, v) -> builder.withCommandOption(new CommandOption(k, v.getName())));
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration class for MQTT component
|
* Configuration class for MQTT component
|
||||||
*/
|
*/
|
||||||
|
@ -39,23 +74,106 @@ public class Scene extends AbstractComponent<Scene.ChannelConfiguration> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SerializedName("command_topic")
|
@SerializedName("command_topic")
|
||||||
protected @Nullable String commandTopic;
|
protected String commandTopic = "";
|
||||||
|
|
||||||
@SerializedName("payload_on")
|
@SerializedName("payload_on")
|
||||||
protected String payloadOn = "ON";
|
protected String payloadOn = "ON";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keeps track of discrete command topics, and one SceneValue that uses that topic
|
||||||
|
private final Map<String, ChannelState> topicsToChannelStates = new HashMap<>();
|
||||||
|
private final Map<String, ChannelConfiguration> objectIdToScene = new TreeMap<>();
|
||||||
|
private final Map<String, ChannelConfiguration> labelToScene = new HashMap<>();
|
||||||
|
|
||||||
|
private final SceneValue value = new SceneValue();
|
||||||
|
private ComponentChannel channel;
|
||||||
|
|
||||||
public Scene(ComponentFactory.ComponentConfiguration componentConfiguration) {
|
public Scene(ComponentFactory.ComponentConfiguration componentConfiguration) {
|
||||||
super(componentConfiguration, ChannelConfiguration.class);
|
super(componentConfiguration, ChannelConfiguration.class);
|
||||||
|
|
||||||
TextValue value = new TextValue(new String[] { channelConfiguration.payloadOn });
|
if (channelConfiguration.commandTopic.isEmpty()) {
|
||||||
|
throw new ConfigurationException("command_topic is required");
|
||||||
|
}
|
||||||
|
|
||||||
buildChannel(SCENE_CHANNEL_ID, ComponentChannelType.STRING, value, getName(),
|
// Name the channel with a constant, not the component ID
|
||||||
|
// So that we only end up with a single channel for all scenes
|
||||||
|
componentId = SCENE_CHANNEL_ID;
|
||||||
|
groupId = null;
|
||||||
|
|
||||||
|
channel = buildChannel(SCENE_CHANNEL_ID, ComponentChannelType.STRING, value, getName(),
|
||||||
componentConfiguration.getUpdateListener())
|
componentConfiguration.getUpdateListener())
|
||||||
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
|
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
|
||||||
channelConfiguration.getQos())
|
channelConfiguration.getQos())
|
||||||
.withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
|
.commandFilter(this::handleCommand).withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
|
||||||
|
topicsToChannelStates.put(channelConfiguration.commandTopic, channel.getState());
|
||||||
|
addScene(this);
|
||||||
|
}
|
||||||
|
|
||||||
finalizeChannels();
|
ComponentChannel getChannel() {
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addScene(Scene scene) {
|
||||||
|
ChannelConfiguration channelConfiguration = scene.getChannelConfiguration();
|
||||||
|
objectIdToScene.put(scene.getHaID().objectID, channelConfiguration);
|
||||||
|
labelToScene.put(channelConfiguration.getName(), channelConfiguration);
|
||||||
|
|
||||||
|
if (!topicsToChannelStates.containsKey(channelConfiguration.commandTopic)) {
|
||||||
|
hiddenChannels.add(scene.getChannel());
|
||||||
|
topicsToChannelStates.put(channelConfiguration.commandTopic, scene.getChannel().getState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean handleCommand(Command command) {
|
||||||
|
// This command has already been processed by the rest of this method,
|
||||||
|
// so just return immediately.
|
||||||
|
if (command instanceof SceneCommand) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String valueStr = command.toString();
|
||||||
|
ChannelConfiguration sceneConfig = objectIdToScene.get(valueStr);
|
||||||
|
if (sceneConfig == null) {
|
||||||
|
sceneConfig = labelToScene.get(command.toString());
|
||||||
|
}
|
||||||
|
if (sceneConfig == null) {
|
||||||
|
throw new IllegalArgumentException("Value " + valueStr + " not within range");
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelState state = Objects.requireNonNull(topicsToChannelStates.get(sceneConfig.commandTopic));
|
||||||
|
// This will end up calling this same method, so be sure no further processing is done
|
||||||
|
state.publishValue(new SceneCommand(sceneConfig.payloadOn));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Scene";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean mergeable(AbstractComponent<?> other) {
|
||||||
|
return other instanceof Scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean merge(AbstractComponent<?> other) {
|
||||||
|
Scene newScene = (Scene) other;
|
||||||
|
Configuration newConfiguration = mergeChannelConfiguration(channel, newScene);
|
||||||
|
|
||||||
|
addScene(newScene);
|
||||||
|
|
||||||
|
// Recreate the channel so that the configuration will have all the scenes
|
||||||
|
stop();
|
||||||
|
channel = buildChannel(SCENE_CHANNEL_ID, ComponentChannelType.STRING, value, "Scene",
|
||||||
|
componentConfiguration.getUpdateListener())
|
||||||
|
.withConfiguration(newConfiguration)
|
||||||
|
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
|
||||||
|
channelConfiguration.getQos())
|
||||||
|
.commandFilter(this::handleCommand).withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
|
||||||
|
// New ChannelState created; need to make sure we're referencing the correct one
|
||||||
|
topicsToChannelStates.put(channelConfiguration.commandTopic, channel.getState());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ public abstract class AbstractChannelConfiguration {
|
||||||
public static final char PARENT_TOPIC_PLACEHOLDER = '~';
|
public static final char PARENT_TOPIC_PLACEHOLDER = '~';
|
||||||
private static final String DEFAULT_THING_NAME = "Home Assistant Device";
|
private static final String DEFAULT_THING_NAME = "Home Assistant Device";
|
||||||
|
|
||||||
protected @Nullable String name;
|
protected String name;
|
||||||
|
|
||||||
protected String icon = "";
|
protected String icon = "";
|
||||||
protected int qos; // defaults to 0 according to HA specification
|
protected int qos; // defaults to 0 according to HA specification
|
||||||
|
@ -136,7 +136,7 @@ public abstract class AbstractChannelConfiguration {
|
||||||
return properties;
|
return properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,6 @@ import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelLinkageChecker;
|
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelLinkageChecker;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
|
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.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.component.Update;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
|
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
||||||
|
@ -482,33 +481,19 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
||||||
private boolean addComponent(AbstractComponent<?> component) {
|
private boolean addComponent(AbstractComponent<?> component) {
|
||||||
AbstractComponent<?> existing = haComponents.get(component.getComponentId());
|
AbstractComponent<?> existing = haComponents.get(component.getComponentId());
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
// DeviceTriggers that are for the same subtype, topic, and value template
|
// Check for components that merge together
|
||||||
// can be coalesced together
|
if (component.mergeable(existing)) {
|
||||||
if (component instanceof DeviceTrigger newTrigger && existing instanceof DeviceTrigger oldTrigger
|
MqttBrokerConnection connection = this.connection;
|
||||||
&& newTrigger.getChannelConfiguration().getSubtype()
|
if (existing.merge(component) && connection != null) {
|
||||||
.equals(oldTrigger.getChannelConfiguration().getSubtype())
|
// Make sure to re-start if this did something, and it was stopped
|
||||||
&& newTrigger.getChannelConfiguration().getTopic()
|
existing.start(connection, scheduler, 0).exceptionally(e -> {
|
||||||
.equals(oldTrigger.getChannelConfiguration().getTopic())
|
logger.warn("Failed to start component {}", existing.getHaID(), e);
|
||||||
&& oldTrigger.getHaID().nodeID.equals(newTrigger.getHaID().nodeID)) {
|
return null;
|
||||||
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);
|
|
||||||
haComponentsByHaId.put(component.getHaID(), component);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
haComponentsByUniqueId.put(component.getUniqueId(), component);
|
||||||
|
haComponentsByHaId.put(component.getHaID(), component);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// rename the conflict
|
// rename the conflict
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.mqtt.homeassistant.internal.component;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import 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.Value;
|
||||||
|
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
|
||||||
|
import org.openhab.core.config.core.Configuration;
|
||||||
|
import org.openhab.core.library.types.StringType;
|
||||||
|
import org.openhab.core.types.CommandOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DeviceTrigger}
|
||||||
|
*
|
||||||
|
* @author Cody Cutrer - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class SceneTests extends AbstractComponentTests {
|
||||||
|
public static final String CONFIG_TOPIC_1 = "scene/12345_14/scene_1";
|
||||||
|
public static final String CONFIG_TOPIC_2 = "scene/12345_14/scene_2";
|
||||||
|
|
||||||
|
@SuppressWarnings("null")
|
||||||
|
@Test
|
||||||
|
public void test() throws InterruptedException {
|
||||||
|
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC_1), """
|
||||||
|
{
|
||||||
|
"command_topic": "zigbee2mqtt/Theater Room Lights/set",
|
||||||
|
"name": "House",
|
||||||
|
"object_id": "theater_room_lights_1_house",
|
||||||
|
"payload_on": "{ \\"scene_recall\\": 1 }",
|
||||||
|
"unique_id": "14_scene_1_zigbee2mqtt"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
assertThat(component.channels.size(), is(1));
|
||||||
|
assertThat(component.getName(), is("Scene"));
|
||||||
|
|
||||||
|
assertChannel(component, Scene.SCENE_CHANNEL_ID, "", "zigbee2mqtt/Theater Room Lights/set", "Scene",
|
||||||
|
Scene.SceneValue.class);
|
||||||
|
linkAllChannels(component);
|
||||||
|
|
||||||
|
component.getChannel(Scene.SCENE_CHANNEL_ID).getState().publishValue(new StringType("scene_1"));
|
||||||
|
assertPublished("zigbee2mqtt/Theater Room Lights/set", "{ \"scene_recall\": 1 }");
|
||||||
|
|
||||||
|
component.getChannel(Scene.SCENE_CHANNEL_ID).getState().publishValue(new StringType("House"));
|
||||||
|
assertPublished("zigbee2mqtt/Theater Room Lights/set", "{ \"scene_recall\": 1 }", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("null")
|
||||||
|
@Test
|
||||||
|
public void testMerge() throws InterruptedException {
|
||||||
|
var component1 = (Scene) discoverComponent(configTopicToMqtt(CONFIG_TOPIC_1), """
|
||||||
|
{
|
||||||
|
"command_topic": "zigbee2mqtt/Theater Room Lights/set",
|
||||||
|
"name": "House",
|
||||||
|
"object_id": "theater_room_lights_1_house",
|
||||||
|
"payload_on": "{ \\"scene_recall\\": 1 }",
|
||||||
|
"unique_id": "14_scene_1_zigbee2mqtt"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
discoverComponent(configTopicToMqtt(CONFIG_TOPIC_2), """
|
||||||
|
{
|
||||||
|
"command_topic": "zigbee2mqtt/Theater Room Lights/set",
|
||||||
|
"name": "Menu",
|
||||||
|
"object_id": "theater_room_lights_2_menu",
|
||||||
|
"payload_on": "{ \\"scene_recall\\": 2 }",
|
||||||
|
"unique_id": "14_scene_2_zigbee2mqtt"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
assertThat(component1.channels.size(), is(1));
|
||||||
|
|
||||||
|
ComponentChannel channel = Objects.requireNonNull(component1.getChannel(Scene.SCENE_CHANNEL_ID));
|
||||||
|
Value value = channel.getState().getCache();
|
||||||
|
List<CommandOption> options = value.createCommandDescription().build().getCommandOptions();
|
||||||
|
assertThat(options.size(), is(2));
|
||||||
|
assertThat(options.get(0).getCommand(), is("scene_1"));
|
||||||
|
assertThat(options.get(1).getCommand(), is("scene_2"));
|
||||||
|
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));
|
||||||
|
|
||||||
|
linkAllChannels(component1);
|
||||||
|
|
||||||
|
component1.getChannel(Scene.SCENE_CHANNEL_ID).getState().publishValue(new StringType("House"));
|
||||||
|
assertPublished("zigbee2mqtt/Theater Room Lights/set", "{ \"scene_recall\": 1 }");
|
||||||
|
|
||||||
|
component1.getChannel(Scene.SCENE_CHANNEL_ID).getState().publishValue(new StringType("scene_2"));
|
||||||
|
assertPublished("zigbee2mqtt/Theater Room Lights/set", "{ \"scene_recall\": 2 }");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("null")
|
||||||
|
@Test
|
||||||
|
public void testMultipleTopics() throws InterruptedException {
|
||||||
|
var component1 = (Scene) discoverComponent(configTopicToMqtt(CONFIG_TOPIC_1), """
|
||||||
|
{
|
||||||
|
"command_topic": "zigbee2mqtt/Theater Room Lights/set",
|
||||||
|
"name": "House",
|
||||||
|
"object_id": "theater_room_lights_1_house",
|
||||||
|
"payload_on": "{ \\"scene_recall\\": 1 }",
|
||||||
|
"unique_id": "14_scene_1_zigbee2mqtt"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
discoverComponent(configTopicToMqtt(CONFIG_TOPIC_2), """
|
||||||
|
{
|
||||||
|
"command_topic": "zigbee2mqtt/Theater Room Lights 2/set",
|
||||||
|
"name": "Menu",
|
||||||
|
"object_id": "theater_room_lights_2_menu",
|
||||||
|
"payload_on": "{ \\"scene_recall\\": 2 }",
|
||||||
|
"unique_id": "14_scene_2_zigbee2mqtt"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
assertThat(component1.channels.size(), is(1));
|
||||||
|
|
||||||
|
ComponentChannel channel = Objects.requireNonNull(component1.getChannel(Scene.SCENE_CHANNEL_ID));
|
||||||
|
Value value = channel.getState().getCache();
|
||||||
|
List<CommandOption> options = value.createCommandDescription().build().getCommandOptions();
|
||||||
|
assertThat(options.size(), is(2));
|
||||||
|
assertThat(options.get(0).getCommand(), is("scene_1"));
|
||||||
|
assertThat(options.get(1).getCommand(), is("scene_2"));
|
||||||
|
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));
|
||||||
|
|
||||||
|
linkAllChannels(component1);
|
||||||
|
|
||||||
|
component1.getChannel(Scene.SCENE_CHANNEL_ID).getState().publishValue(new StringType("House"));
|
||||||
|
assertPublished("zigbee2mqtt/Theater Room Lights/set", "{ \"scene_recall\": 1 }");
|
||||||
|
|
||||||
|
component1.getChannel(Scene.SCENE_CHANNEL_ID).getState().publishValue(new StringType("scene_2"));
|
||||||
|
assertPublished("zigbee2mqtt/Theater Room Lights 2/set", "{ \"scene_recall\": 2 }");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Set<String> getConfigTopics() {
|
||||||
|
return Set.of(CONFIG_TOPIC_1, CONFIG_TOPIC_2);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue