mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[mqtt.homeassistant] Use a single channel for all events from a single button (#17598)
Use the `subtype` field to collapse multiple DeviceAutomation components into a single channel. Signed-off-by: Cody Cutrer <cody@cutrer.us> Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
parent
41d19067bf
commit
89b4e2267d
@ -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)
|
||||
|
@ -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
|
||||
|
@ -12,12 +12,18 @@
|
||||
*/
|
||||
package org.openhab.binding.mqtt.homeassistant.internal.component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.mqtt.generic.values.TextValue;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
@ -31,7 +37,7 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
|
||||
/**
|
||||
* Configuration class for MQTT component
|
||||
*/
|
||||
static class ChannelConfiguration extends AbstractChannelConfiguration {
|
||||
public static class ChannelConfiguration extends AbstractChannelConfiguration {
|
||||
ChannelConfiguration() {
|
||||
super("MQTT Device Trigger");
|
||||
}
|
||||
@ -43,6 +49,14 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
|
||||
protected String subtype = "";
|
||||
|
||||
protected @Nullable String payload;
|
||||
|
||||
public String getTopic() {
|
||||
return topic;
|
||||
}
|
||||
|
||||
public String getSubtype() {
|
||||
return subtype;
|
||||
}
|
||||
}
|
||||
|
||||
public DeviceTrigger(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
|
||||
@ -58,6 +72,14 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
|
||||
throw new ConfigurationException("Component:DeviceTrigger must have a subtype");
|
||||
}
|
||||
|
||||
if (newStyleChannels) {
|
||||
// Name the channel after the subtype, not the component ID
|
||||
// So that we only end up with a single channel for all possible events
|
||||
// for a single button (subtype is the button, type is type of press)
|
||||
componentId = channelConfiguration.subtype;
|
||||
groupId = null;
|
||||
}
|
||||
|
||||
TextValue value;
|
||||
String payload = channelConfiguration.payload;
|
||||
if (payload != null) {
|
||||
@ -69,6 +91,54 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
|
||||
buildChannel(channelConfiguration.type, ComponentChannelType.TRIGGER, value, getName(),
|
||||
componentConfiguration.getUpdateListener())
|
||||
.stateTopic(channelConfiguration.topic, channelConfiguration.getValueTemplate()).trigger(true).build();
|
||||
finalizeChannels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Take another DeviceTrigger (presumably whose subtype, topic, and value template match),
|
||||
* and adjust this component's channel to accept the payload that trigger allows.
|
||||
*
|
||||
* @return if the component was stopped, and thus needs restarted
|
||||
*/
|
||||
|
||||
public boolean merge(DeviceTrigger other) {
|
||||
ComponentChannel channel = channels.get(componentId);
|
||||
TextValue value = (TextValue) channel.getState().getCache();
|
||||
Set<String> payloads = value.getStates();
|
||||
// Append objectid/config to channel configuration
|
||||
Configuration currentConfiguration = channel.getChannel().getConfiguration();
|
||||
Configuration newConfiguration = new Configuration();
|
||||
newConfiguration.put("component", currentConfiguration.get("component"));
|
||||
newConfiguration.put("nodeid", currentConfiguration.get("nodeid"));
|
||||
Object objectIdObject = currentConfiguration.get("objectid");
|
||||
if (objectIdObject instanceof String objectIdString) {
|
||||
newConfiguration.put("objectid", List.of(objectIdString, other.getHaID().objectID));
|
||||
} else if (objectIdObject instanceof List<?> objectIdList) {
|
||||
newConfiguration.put("objectid",
|
||||
Stream.concat(objectIdList.stream(), Stream.of(other.getHaID().objectID)).toList());
|
||||
}
|
||||
Object configObject = currentConfiguration.get("config");
|
||||
if (configObject instanceof String configString) {
|
||||
newConfiguration.put("config", List.of(configString, other.getChannelConfigurationJson()));
|
||||
} else if (configObject instanceof List<?> configList) {
|
||||
newConfiguration.put("config",
|
||||
Stream.concat(configList.stream(), Stream.of(other.getChannelConfigurationJson())).toList());
|
||||
}
|
||||
|
||||
// Append payload to allowed values
|
||||
String otherPayload = other.getChannelConfiguration().payload;
|
||||
if (payloads == null || otherPayload == null) {
|
||||
// Need to accept anything
|
||||
value = new TextValue();
|
||||
} else {
|
||||
String[] newValues = Stream.concat(payloads.stream(), Stream.of(otherPayload)).toArray(String[]::new);
|
||||
value = new TextValue(newValues);
|
||||
}
|
||||
|
||||
// Recreate the channel
|
||||
stop();
|
||||
buildChannel(channelConfiguration.type, ComponentChannelType.TRIGGER, value, componentId,
|
||||
componentConfiguration.getUpdateListener()).withConfiguration(newConfiguration)
|
||||
.stateTopic(channelConfiguration.topic, channelConfiguration.getValueTemplate()).trigger(true).build();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,11 @@
|
||||
package org.openhab.binding.mqtt.homeassistant.internal.handler;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@ -39,6 +41,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.component.DeviceTrigger;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.component.Update;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
||||
@ -156,35 +159,39 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Configuration channelConfig = channel.getConfiguration();
|
||||
if (!channelConfig.containsKey("component")
|
||||
|| !channelConfig.containsKey("objectid") | !channelConfig.containsKey("config")) {
|
||||
Configuration multiComponentChannelConfig = channel.getConfiguration();
|
||||
if (!multiComponentChannelConfig.containsKey("component")
|
||||
|| !multiComponentChannelConfig.containsKey("objectid")
|
||||
|| !multiComponentChannelConfig.containsKey("config")) {
|
||||
// Must be a secondary channel
|
||||
continue;
|
||||
}
|
||||
|
||||
HaID haID = HaID.fromConfig(config.basetopic, channelConfig);
|
||||
List<Configuration> flattenedConfig = flattenChannelConfiguration(multiComponentChannelConfig,
|
||||
channel.getUID());
|
||||
for (Configuration channelConfig : flattenedConfig) {
|
||||
HaID haID = HaID.fromConfig(config.basetopic, channelConfig);
|
||||
|
||||
if (!config.topics.contains(haID.getTopic())) {
|
||||
// don't add a component for this channel that isn't configured on the thing
|
||||
// anymore
|
||||
// It will disappear from the thing when the thing type is updated below
|
||||
continue;
|
||||
}
|
||||
|
||||
discoveryHomeAssistantIDs.add(haID);
|
||||
ThingUID thingUID = channel.getUID().getThingUID();
|
||||
String channelConfigurationJSON = (String) channelConfig.get("config");
|
||||
try {
|
||||
AbstractComponent<?> component = ComponentFactory.createComponent(thingUID, haID,
|
||||
channelConfigurationJSON, this, this, scheduler, gson, jinjava, newStyleChannels);
|
||||
if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
|
||||
typeID = calculateThingTypeUID(component);
|
||||
if (!config.topics.contains(haID.getTopic())) {
|
||||
// don't add a component for this channel that isn't configured on the thing
|
||||
// anymore. It will disappear from the thing when the thing type is updated below
|
||||
continue;
|
||||
}
|
||||
|
||||
addComponent(component);
|
||||
} catch (ConfigurationException e) {
|
||||
logger.warn("Cannot restore component {}: {}", thing, e.getMessage());
|
||||
discoveryHomeAssistantIDs.add(haID);
|
||||
ThingUID thingUID = channel.getUID().getThingUID();
|
||||
String channelConfigurationJSON = (String) channelConfig.get("config");
|
||||
try {
|
||||
AbstractComponent<?> component = ComponentFactory.createComponent(thingUID, haID,
|
||||
channelConfigurationJSON, this, this, scheduler, gson, jinjava, newStyleChannels);
|
||||
if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
|
||||
typeID = calculateThingTypeUID(component);
|
||||
}
|
||||
|
||||
addComponent(component);
|
||||
} catch (ConfigurationException e) {
|
||||
logger.warn("Cannot restore component {}: {}", thing, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updateThingType(typeID)) {
|
||||
@ -280,7 +287,8 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
||||
AbstractComponent<?> known = haComponentsByUniqueId.get(discovered.getUniqueId());
|
||||
// Is component already known?
|
||||
if (known != null) {
|
||||
if (discovered.getConfigHash() != known.getConfigHash()) {
|
||||
if (discovered.getConfigHash() != known.getConfigHash()
|
||||
&& discovered.getUniqueId().equals(known.getUniqueId())) {
|
||||
// Don't wait for the future to complete. We are also not interested in failures.
|
||||
// The component will be replaced in a moment.
|
||||
known.stop();
|
||||
@ -422,6 +430,35 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
||||
private void addComponent(AbstractComponent component) {
|
||||
AbstractComponent existing = haComponents.get(component.getComponentId());
|
||||
if (existing != null) {
|
||||
// DeviceTriggers that are for the same subtype, topic, and value template
|
||||
// can be coalesced together
|
||||
if (component instanceof DeviceTrigger newTrigger && existing instanceof DeviceTrigger oldTrigger
|
||||
&& newTrigger.getChannelConfiguration().getSubtype()
|
||||
.equals(oldTrigger.getChannelConfiguration().getSubtype())
|
||||
&& newTrigger.getChannelConfiguration().getTopic()
|
||||
.equals(oldTrigger.getChannelConfiguration().getTopic())
|
||||
&& oldTrigger.getHaID().nodeID.equals(newTrigger.getHaID().nodeID)) {
|
||||
String newTriggerValueTemplate = newTrigger.getChannelConfiguration().getValueTemplate();
|
||||
String oldTriggerValueTemplate = oldTrigger.getChannelConfiguration().getValueTemplate();
|
||||
if ((newTriggerValueTemplate == null && oldTriggerValueTemplate == null)
|
||||
|| (newTriggerValueTemplate != null & oldTriggerValueTemplate != null
|
||||
&& newTriggerValueTemplate.equals(oldTriggerValueTemplate))) {
|
||||
// Adjust the set of valid values
|
||||
MqttBrokerConnection connection = this.connection;
|
||||
|
||||
if (oldTrigger.merge(newTrigger) && connection != null) {
|
||||
// Make sure to re-start if this did something, and it was stopped
|
||||
oldTrigger.start(connection, scheduler, 0).exceptionally(e -> {
|
||||
logger.warn("Failed to start component {}", oldTrigger.getHaID(), e);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
haComponentsByUniqueId.put(component.getUniqueId(), component);
|
||||
System.out.println("don't forget to add to the channel config");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// rename the conflict
|
||||
haComponents.remove(existing.getComponentId());
|
||||
existing.resolveConflict();
|
||||
@ -431,4 +468,41 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
||||
haComponents.put(component.getComponentId(), component);
|
||||
haComponentsByUniqueId.put(component.getUniqueId(), component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a Configuration where objectid and config are a list, and generates
|
||||
* multiple Configurations where there are single objects
|
||||
*/
|
||||
private List<Configuration> flattenChannelConfiguration(Configuration multiComponentChannelConfig,
|
||||
ChannelUID channelUID) {
|
||||
Object component = multiComponentChannelConfig.get("component");
|
||||
Object nodeid = multiComponentChannelConfig.get("nodeid");
|
||||
if ((multiComponentChannelConfig.get("objectid") instanceof List objectIds)
|
||||
&& (multiComponentChannelConfig.get("config") instanceof List configurations)) {
|
||||
if (objectIds.size() != configurations.size()) {
|
||||
logger.warn("objectid and config for channel {} do not have the same number of items; ignoring",
|
||||
channelUID);
|
||||
return List.of();
|
||||
}
|
||||
List<Configuration> result = new ArrayList();
|
||||
Iterator<Object> objectIdIterator = objectIds.iterator();
|
||||
Iterator<Object> configIterator = configurations.iterator();
|
||||
while (objectIdIterator.hasNext()) {
|
||||
Configuration componentConfiguration = new Configuration();
|
||||
componentConfiguration.put("component", component);
|
||||
componentConfiguration.put("nodeid", nodeid);
|
||||
componentConfiguration.put("objectid", objectIdIterator.next());
|
||||
componentConfiguration.put("config", configIterator.next());
|
||||
result.add(componentConfiguration);
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return List.of(multiComponentChannelConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// For testing
|
||||
Map<@Nullable String, AbstractComponent<?>> getComponents() {
|
||||
return haComponents;
|
||||
}
|
||||
}
|
||||
|
@ -15,12 +15,12 @@
|
||||
<description>Optional node name of the component</description>
|
||||
<default></default>
|
||||
</parameter>
|
||||
<parameter name="objectid" type="text" readOnly="true">
|
||||
<parameter name="objectid" type="text" readOnly="true" multiple="true">
|
||||
<label>Object ID</label>
|
||||
<description>Object ID of the component</description>
|
||||
<default></default>
|
||||
</parameter>
|
||||
<parameter name="config" type="text" readOnly="true">
|
||||
<parameter name="config" type="text" readOnly="true" multiple="true">
|
||||
<label>JSON Configuration</label>
|
||||
<description>The JSON configuration string received by the component via MQTT.</description>
|
||||
<default></default>
|
||||
|
@ -94,7 +94,7 @@ public abstract class AbstractHomeAssistantTests extends JavaTest {
|
||||
|
||||
protected final Bridge bridgeThing = BridgeBuilder.create(BRIDGE_TYPE_UID, BRIDGE_UID).build();
|
||||
protected final BrokerHandler bridgeHandler = spy(new BrokerHandler(bridgeThing));
|
||||
protected final Thing haThing = ThingBuilder.create(HA_TYPE_UID, HA_UID).withBridge(BRIDGE_UID).build();
|
||||
protected Thing haThing = ThingBuilder.create(HA_TYPE_UID, HA_UID).withBridge(BRIDGE_UID).build();
|
||||
protected final ConcurrentMap<String, Set<MqttMessageSubscriber>> subscriptions = new ConcurrentHashMap<>();
|
||||
|
||||
private @Mock @NonNullByDefault({}) TransformationService transformationService1Mock;
|
||||
|
@ -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();
|
||||
|
@ -80,6 +80,9 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
|
||||
|
||||
when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
|
||||
|
||||
if (useNewStyleChannels()) {
|
||||
haThing.setProperty("newStyleChannels", "true");
|
||||
}
|
||||
thingHandler = new LatchThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
|
||||
channelTypeRegistry, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
|
||||
thingHandler.setConnection(bridgeConnection);
|
||||
@ -104,6 +107,13 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
|
||||
*/
|
||||
protected abstract Set<String> getConfigTopics();
|
||||
|
||||
/**
|
||||
* If new style channels should be used for this test.
|
||||
*/
|
||||
protected boolean useNewStyleChannels() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
|
||||
*
|
||||
|
@ -14,12 +14,18 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.mqtt.generic.values.TextValue;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
|
||||
/**
|
||||
* Tests for {@link DeviceTrigger}
|
||||
@ -28,12 +34,13 @@ import org.openhab.binding.mqtt.generic.values.TextValue;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DeviceTriggerTests extends AbstractComponentTests {
|
||||
public static final String CONFIG_TOPIC = "device_automation/0x8cf681fffe2fd2a6";
|
||||
public static final String CONFIG_TOPIC_1 = "device_automation/0x8cf681fffe2fd2a6/press";
|
||||
public static final String CONFIG_TOPIC_2 = "device_automation/0x8cf681fffe2fd2a6/release";
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
public void test() throws InterruptedException {
|
||||
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
|
||||
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC_1), """
|
||||
{
|
||||
"automation_type": "trigger",
|
||||
"device": {
|
||||
@ -56,19 +63,93 @@ public class DeviceTriggerTests extends AbstractComponentTests {
|
||||
assertThat(component.channels.size(), is(1));
|
||||
assertThat(component.getName(), is("MQTT Device Trigger"));
|
||||
|
||||
assertChannel(component, "action", "zigbee2mqtt/Charge Now Button/action", "", "MQTT Device Trigger",
|
||||
assertChannel(component, "on", "zigbee2mqtt/Charge Now Button/action", "", "MQTT Device Trigger",
|
||||
TextValue.class);
|
||||
|
||||
spyOnChannelUpdates(component, "action");
|
||||
spyOnChannelUpdates(component, "on");
|
||||
publishMessage("zigbee2mqtt/Charge Now Button/action", "on");
|
||||
assertTriggered(component, "action", "on");
|
||||
assertTriggered(component, "on", "on");
|
||||
|
||||
publishMessage("zigbee2mqtt/Charge Now Button/action", "off");
|
||||
assertNotTriggered(component, "action", "off");
|
||||
assertNotTriggered(component, "on", "off");
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
public void testMerge() throws InterruptedException {
|
||||
var component1 = (DeviceTrigger) discoverComponent(configTopicToMqtt(CONFIG_TOPIC_1), """
|
||||
{
|
||||
"automation_type": "trigger",
|
||||
"device": {
|
||||
"configuration_url": "#/device/0x8cf681fffe2fd2a6/info",
|
||||
"identifiers": [
|
||||
"zigbee2mqtt_0x8cf681fffe2fd2a6"
|
||||
],
|
||||
"manufacturer": "IKEA",
|
||||
"model": "TRADFRI shortcut button (E1812)",
|
||||
"name": "Charge Now Button",
|
||||
"sw_version": "2.3.015"
|
||||
},
|
||||
"payload": "press",
|
||||
"subtype": "turn_on",
|
||||
"topic": "zigbee2mqtt/Charge Now Button/action",
|
||||
"type": "button_long_press"
|
||||
}
|
||||
""");
|
||||
var component2 = (DeviceTrigger) discoverComponent(configTopicToMqtt(CONFIG_TOPIC_2), """
|
||||
{
|
||||
"automation_type": "trigger",
|
||||
"device": {
|
||||
"configuration_url": "#/device/0x8cf681fffe2fd2a6/info",
|
||||
"identifiers": [
|
||||
"zigbee2mqtt_0x8cf681fffe2fd2a6"
|
||||
],
|
||||
"manufacturer": "IKEA",
|
||||
"model": "TRADFRI shortcut button (E1812)",
|
||||
"name": "Charge Now Button",
|
||||
"sw_version": "2.3.015"
|
||||
},
|
||||
"payload": "release",
|
||||
"subtype": "turn_on",
|
||||
"topic": "zigbee2mqtt/Charge Now Button/action",
|
||||
"type": "button_long_release"
|
||||
}
|
||||
""");
|
||||
|
||||
assertThat(component1.channels.size(), is(1));
|
||||
|
||||
ComponentChannel channel = Objects.requireNonNull(component1.getChannel("turn_on"));
|
||||
TextValue value = (TextValue) channel.getState().getCache();
|
||||
Set<String> payloads = value.getStates();
|
||||
assertNotNull(payloads);
|
||||
assertThat(payloads.size(), is(2));
|
||||
assertThat(payloads.contains("press"), is(true));
|
||||
assertThat(payloads.contains("release"), is(true));
|
||||
Configuration channelConfig = channel.getChannel().getConfiguration();
|
||||
Object config = channelConfig.get("config");
|
||||
assertNotNull(config);
|
||||
assertThat(config.getClass(), is(ArrayList.class));
|
||||
List<?> configList = (List<?>) config;
|
||||
assertThat(configList.size(), is(2));
|
||||
|
||||
spyOnChannelUpdates(component1, "turn_on");
|
||||
publishMessage("zigbee2mqtt/Charge Now Button/action", "press");
|
||||
assertTriggered(component1, "turn_on", "press");
|
||||
|
||||
publishMessage("zigbee2mqtt/Charge Now Button/action", "release");
|
||||
assertTriggered(component1, "turn_on", "release");
|
||||
|
||||
publishMessage("zigbee2mqtt/Charge Now Button/action", "otherwise");
|
||||
assertNotTriggered(component1, "turn_on", "otherwise");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Set<String> getConfigTopics() {
|
||||
return Set.of(CONFIG_TOPIC);
|
||||
return Set.of(CONFIG_TOPIC_1, CONFIG_TOPIC_2);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean useNewStyleChannels() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user