diff --git a/addons/binding/org.openhab.binding.mqtt.generic.test/src/test/java/org/openhab/binding/mqtt/generic/HomeAssistantMQTTImplementationTests.java b/addons/binding/org.openhab.binding.mqtt.generic.test/src/test/java/org/openhab/binding/mqtt/generic/HomeAssistantMQTTImplementationTests.java index b279a23aeb81d..9589e92137e1a 100644 --- a/addons/binding/org.openhab.binding.mqtt.generic.test/src/test/java/org/openhab/binding/mqtt/generic/HomeAssistantMQTTImplementationTests.java +++ b/addons/binding/org.openhab.binding.mqtt.generic.test/src/test/java/org/openhab/binding/mqtt/generic/HomeAssistantMQTTImplementationTests.java @@ -45,10 +45,10 @@ import org.junit.Test; import org.mockito.Mock; import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.AbstractComponent; +import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.ChannelConfigurationTypeAdapterFactory; import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.ComponentSwitch; import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.DiscoverComponents; import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.DiscoverComponents.ComponentDiscovered; -import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.ChannelConfigurationTypeAdapterFactory; import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.HaID; import org.openhab.binding.mqtt.generic.internal.generic.ChannelStateUpdateListener; import org.openhab.binding.mqtt.generic.internal.generic.MqttChannelTypeProvider; @@ -175,7 +175,7 @@ public void parseHATree() throws InterruptedException, ExecutionException, Timeo // and add the types to the channelTypeProvider, like in the real Thing handler. final CountDownLatch latch = new CountDownLatch(1); ComponentDiscovered cd = (haID, c) -> { - haComponents.put(haID.getChannelGroupID(), c); + haComponents.put(c.uid().getId(), c); c.addChannelTypes(channelTypeProvider); channelTypeProvider.setChannelGroupType(c.groupTypeUID(), c.type()); latch.countDown(); @@ -192,7 +192,7 @@ public void parseHATree() throws InterruptedException, ExecutionException, Timeo assertTrue(latch.await(300, TimeUnit.MILLISECONDS)); future.get(100, TimeUnit.MILLISECONDS); - // No failure expected and one discoverd result + // No failure expected and one discovered result assertNull(failure); assertThat(haComponents.size(), is(1)); @@ -200,9 +200,11 @@ public void parseHATree() throws InterruptedException, ExecutionException, Timeo verify(channelTypeProvider, times(1)).setChannelGroupType(any(), any()); verify(channelTypeProvider, times(1)).setChannelType(any(), any()); + String channelGroupId = ThingChannelConstants.testHomeAssistantThing.getId() + "_switch"; + // We expect a switch component with an OnOff channel with the initial value UNDEF: - State value = haComponents.get(haID.getChannelGroupID()).channelTypes().get(ComponentSwitch.switchChannelID) - .getState().getCache().getChannelState(); + State value = haComponents.get(channelGroupId).channelTypes().get(ComponentSwitch.switchChannelID).getState() + .getCache().getChannelState(); assertThat(value, is(UnDefType.UNDEF)); haComponents.values().stream().map(e -> e.start(connection, scheduler, 100)) @@ -215,8 +217,8 @@ public void parseHATree() throws InterruptedException, ExecutionException, Timeo verify(channelStateUpdateListener, times(1)).updateChannelState(any(), any()); // Value should be ON now. - value = haComponents.get(haID.getChannelGroupID()).channelTypes().get(ComponentSwitch.switchChannelID) - .getState().getCache().getChannelState(); + value = haComponents.get(channelGroupId).channelTypes().get(ComponentSwitch.switchChannelID).getState() + .getCache().getChannelState(); assertThat(value, is(OnOffType.ON)); } diff --git a/addons/binding/org.openhab.binding.mqtt.generic.test/src/test/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/DiscoverComponentsTests.java b/addons/binding/org.openhab.binding.mqtt.generic.test/src/test/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/DiscoverComponentsTests.java index 3e1421616421d..0b833cfcd9795 100644 --- a/addons/binding/org.openhab.binding.mqtt.generic.test/src/test/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/DiscoverComponentsTests.java +++ b/addons/binding/org.openhab.binding.mqtt.generic.test/src/test/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/DiscoverComponentsTests.java @@ -74,8 +74,9 @@ public void discoveryTimeTest() throws InterruptedException, ExecutionException, DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.testHomeAssistantThing, scheduler, null, gson, transformationServiceProvider)); - discover.startDiscovery(connection, 50, new HaID("homeassistant", "object", "node", "component"), discovered) - .get(100, TimeUnit.MILLISECONDS); + HandlerConfiguration config = new HandlerConfiguration("homeassistant", "object"); + + discover.startDiscovery(connection, 50, HaID.fromConfig(config), discovered).get(100, TimeUnit.MILLISECONDS); } } diff --git a/addons/binding/org.openhab.binding.mqtt.generic.test/src/test/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/HaIDTests.java b/addons/binding/org.openhab.binding.mqtt.generic.test/src/test/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/HaIDTests.java index d4f6dceb624a4..e1353ff1eb2b7 100644 --- a/addons/binding/org.openhab.binding.mqtt.generic.test/src/test/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/HaIDTests.java +++ b/addons/binding/org.openhab.binding.mqtt.generic.test/src/test/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/HaIDTests.java @@ -15,9 +15,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; -import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.config.core.Configuration; import org.junit.Test; -import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.HaID; public class HaIDTests { @@ -25,20 +24,44 @@ public class HaIDTests { public void testWithoutNode() { HaID subject = new HaID("homeassistant/switch/name/config"); - assertThat(subject.getThingID(), is("name")); - assertThat(subject.getChannelGroupTypeID(), is("name_switch")); - assertThat(subject.getChannelTypeID("channel"), is(new ChannelTypeUID("mqtt:name_switch_channel"))); - assertThat(subject.getChannelGroupID(), is("switch_")); + assertThat(subject.objectID, is("name")); + + assertThat(subject.component, is("switch")); + assertThat(subject.getTopic("suffix"), is("homeassistant/switch/name/suffix")); + + Configuration config = new Configuration(); + subject.toConfig(config); + + HaID restore = HaID.fromConfig("homeassistant", config); + + assertThat(restore, is(subject)); + + HandlerConfiguration haConfig = subject.toHandlerConfiguration(); + + restore = HaID.fromConfig(haConfig); + assertThat(restore, is(new HaID("homeassistant/+/name/config"))); } @Test public void testWithNode() { HaID subject = new HaID("homeassistant/switch/node/name/config"); - assertThat(subject.getThingID(), is("name")); - assertThat(subject.getChannelGroupTypeID(), is("name_switchnode")); - assertThat(subject.getChannelTypeID("channel"), is(new ChannelTypeUID("mqtt:name_switchnode_channel"))); - assertThat(subject.getChannelGroupID(), is("switch_node")); + assertThat(subject.objectID, is("name")); + + assertThat(subject.component, is("switch")); + assertThat(subject.getTopic("suffix"), is("homeassistant/switch/node/name/suffix")); + + Configuration config = new Configuration(); + subject.toConfig(config); + + HaID restore = HaID.fromConfig("homeassistant", config); + + assertThat(restore, is(subject)); + + HandlerConfiguration haConfig = subject.toHandlerConfiguration(); + + restore = HaID.fromConfig(haConfig); + assertThat(restore, is(new HaID("homeassistant/+/node/name/config"))); } } diff --git a/addons/binding/org.openhab.binding.mqtt.generic/ESH-INF/config/homeassistant-channel-config.xml b/addons/binding/org.openhab.binding.mqtt.generic/ESH-INF/config/homeassistant-channel-config.xml index 4d162e12bcceb..f510eb45bdfb6 100644 --- a/addons/binding/org.openhab.binding.mqtt.generic/ESH-INF/config/homeassistant-channel-config.xml +++ b/addons/binding/org.openhab.binding.mqtt.generic/ESH-INF/config/homeassistant-channel-config.xml @@ -5,7 +5,22 @@ xsi:schemaLocation="http://eclipse.org/smarthome/schemas/config-description/v1.0.0 http://eclipse.org/smarthome/schemas/config-description-1.0.0.xsd"> - + + + Type of the channel group. + + + + + Optional node name of the component + + + + + Object id of the component + + + The json configuration string received by the component via MQTT. diff --git a/addons/binding/org.openhab.binding.mqtt.generic/README.md b/addons/binding/org.openhab.binding.mqtt.generic/README.md index dfc887ba5c1b4..2cfbf8db30444 100644 --- a/addons/binding/org.openhab.binding.mqtt.generic/README.md +++ b/addons/binding/org.openhab.binding.mqtt.generic/README.md @@ -23,6 +23,7 @@ Find the next table to understand the topology mapping from Homie to the Framewo | Property | Channel | homie/super-car/engine/temperature | System trigger channels are supported using non-retained properties, with *enum* data type and with the following formats: + * Format: "PRESSED,RELEASED" -> system.rawbutton * Format: "SHORT\_PRESSED,DOUBLE\_PRESSED,LONG\_PRESSED" -> system.button * Format: "DIR1\_PRESSED,DIR1\_RELEASED,DIR2\_PRESSED,DIR2\_RELEASED" -> system.rawrocker @@ -124,6 +125,7 @@ You can connect this channel to a Contact or Switch item. * __on__: An optional string (like "BRIGHT") that is recognized as on state. (ON will always be recognized.) * __off__: An optional string (like "DARK") that is recognized as off state. (OFF will always be recognized.) * __onBrightness__: If you connect this channel to a Switch item and turn it on, + color and saturation are preserved from the last state, but the brightness will be set to this configured initial brightness (default: 10%). @@ -234,9 +236,9 @@ Here are a few examples: ## Troubleshooting * If you get the error "No MQTT client": Please update your installation. -* If you use the Mosquitto broker: Please be aware that there is a relatively low setting - for retained messages. At some point messages will just not being delivered - anymore: Change the setting +* If you use the Mosquitto broker: Please be aware that there is a relatively low setting +for retained messages. At some point messages will just not being delivered anymore: +Change the setting ## Examples diff --git a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/AbstractComponent.java b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/AbstractComponent.java index c4484b658f411..96f7c41c51c4d 100644 --- a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/AbstractComponent.java +++ b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/AbstractComponent.java @@ -19,6 +19,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.stream.Collectors; +import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.ChannelGroupUID; @@ -65,14 +66,20 @@ public abstract class AbstractComponent { */ public AbstractComponent(CFactory.ComponentConfiguration componentConfiguration, Class clazz) { this.componentConfiguration = componentConfiguration; - this.haID = componentConfiguration.getHaID(); - this.channelGroupTypeUID = new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, - haID.getChannelGroupTypeID()); - this.channelGroupUID = new ChannelGroupUID(componentConfiguration.getThingUID(), haID.getChannelGroupID()); this.channelConfigurationJson = componentConfiguration.getConfigJSON(); this.channelConfiguration = componentConfiguration.getConfig(clazz); this.configHash = channelConfigurationJson.hashCode(); + + this.haID = componentConfiguration.getHaID(); + + String groupId = channelConfiguration.unique_id; + if (groupId == null || StringUtils.isBlank(groupId)) { + groupId = this.haID.getFallbackGroupId(); + } + + this.channelGroupTypeUID = new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, groupId); + this.channelGroupUID = new ChannelGroupUID(componentConfiguration.getThingUID(), groupId); } protected CChannel.Builder buildChannel(String channelID, Value valueState, String label) { diff --git a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/CChannel.java b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/CChannel.java index be06e5ec628e3..d1b0fcfdcfb98 100644 --- a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/CChannel.java +++ b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/CChannel.java @@ -180,7 +180,8 @@ public CChannel build(boolean addToComponent) { ChannelTypeUID channelTypeUID; channelUID = new ChannelUID(component.channelGroupUID, channelID); - channelTypeUID = component.haID.getChannelTypeID(channelID); + channelTypeUID = new ChannelTypeUID(MqttBindingConstants.BINDING_ID, + channelUID.getGroupId() + "_" + channelID); channelState = new ChannelState( ChannelConfigBuilder.create().withRetain(retain).withStateTopic(state_topic) .withCommandTopic(command_topic).build(), @@ -197,6 +198,8 @@ public CChannel build(boolean addToComponent) { Configuration configuration = new Configuration(); configuration.put("config", component.channelConfigurationJson); + component.haID.toConfig(configuration); + channel = ChannelBuilder.create(channelUID, channelState.getItemType()).withType(channelTypeUID) .withKind(type.getKind()).withLabel(label).withConfiguration(configuration).build(); diff --git a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/CFactory.java b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/CFactory.java index 721cace588359..79a024956580e 100644 --- a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/CFactory.java +++ b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/CFactory.java @@ -14,7 +14,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.smarthome.core.thing.Channel; import org.eclipse.smarthome.core.thing.ThingUID; import org.openhab.binding.mqtt.generic.internal.generic.ChannelStateUpdateListener; import org.openhab.binding.mqtt.generic.internal.generic.TransformationServiceProvider; @@ -44,11 +43,12 @@ public class CFactory { * @param updateListener A channel state update listener * @return A HA MQTT Component */ - public static @Nullable AbstractComponent createComponent(ThingUID thingUID, HaID haID, String configJSON, - @Nullable ChannelStateUpdateListener updateListener, Gson gson, + public static @Nullable AbstractComponent createComponent(ThingUID thingUID, HaID haID, + String channelConfigurationJSON, @Nullable ChannelStateUpdateListener updateListener, Gson gson, TransformationServiceProvider transformationServiceProvider) { - ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID, configJSON, gson) - .listener(updateListener).transformationProvider(transformationServiceProvider); + ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID, + channelConfigurationJSON, gson).listener(updateListener) + .transformationProvider(transformationServiceProvider); try { switch (haID.component) { case "alarm_control_panel": @@ -78,27 +78,6 @@ public class CFactory { return null; } - /** - * Create a HA MQTT component by a given channel configuration. - * - * @param basetopic The MQTT base topic, usually "homeassistant" - * @param channel A channel with the JSON configuration embedded as configuration (key: 'config') - * @param updateListener A channel state update listener - * @return A HA MQTT Component - */ - public static @Nullable AbstractComponent createComponent(String basetopic, Channel channel, - @Nullable ChannelStateUpdateListener updateListener, Gson gson, - TransformationServiceProvider transformationServiceProvider) { - HaID haID = new HaID(basetopic, channel.getUID()); - ThingUID thingUID = channel.getUID().getThingUID(); - String configJSON = (String) channel.getConfiguration().get("config"); - if (configJSON == null) { - logger.warn("Provided channel does not have a 'config' configuration key!"); - return null; - } - return createComponent(thingUID, haID, configJSON, updateListener, gson, transformationServiceProvider); - } - protected static class ComponentConfiguration { private ThingUID thingUID; private HaID haID; diff --git a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/DiscoverComponents.java b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/DiscoverComponents.java index 451dc11cf90bb..24f8b394e5ba2 100644 --- a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/DiscoverComponents.java +++ b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/DiscoverComponents.java @@ -51,7 +51,6 @@ public class DiscoverComponents implements MqttMessageSubscriber { private WeakReference<@Nullable MqttBrokerConnection> connectionRef = new WeakReference<>(null); protected @NonNullByDefault({}) ComponentDiscovered discoveredListener; private int discoverTime; - private String topicWithNode = ""; private String topic = ""; /** @@ -106,8 +105,10 @@ public void processMessage(String topic, byte[] payload) { *

* * @param connection A MQTT broker connection - * @param discoverTime The time in milliseconds for the discovery to run. Can be 0 to disable the timeout. - * You need to call {@link #stopDiscovery(MqttBrokerConnection)} at some point in that case. + * @param discoverTime The time in milliseconds for the discovery to run. Can be 0 to disable the + * timeout. + * You need to call {@link #stopDiscovery(MqttBrokerConnection)} at some + * point in that case. * @param topicDescription Contains the object-id (=device id) and potentially a node-id as well. * @param componentsDiscoveredListener Listener for results * @return A future that completes normally after the given time in milliseconds or exceptionally on any error. @@ -116,15 +117,13 @@ public void processMessage(String topic, byte[] payload) { public CompletableFuture<@Nullable Void> startDiscovery(MqttBrokerConnection connection, int discoverTime, HaID topicDescription, ComponentDiscovered componentsDiscoveredListener) { - this.topicWithNode = topicDescription.baseTopic + "/+/+/" + topicDescription.objectID + "/config"; - this.topic = topicDescription.baseTopic + "/+/" + topicDescription.objectID + "/config"; + this.topic = topicDescription.getTopic("config"); this.discoverTime = discoverTime; this.discoveredListener = componentsDiscoveredListener; this.connectionRef = new WeakReference<>(connection); - // Subscribe to the wildcard topics and start receive MQTT retained topics - CompletableFuture.allOf(connection.subscribe(topic, this), connection.subscribe(topicWithNode, this)) - .thenRun(this::subscribeSuccess).exceptionally(this::subscribeFail); + // Subscribe to the wildcard topic and start receive MQTT retained topics + connection.subscribe(topic, this).thenRun(this::subscribeSuccess).exceptionally(this::subscribeFail); return discoverFinishedFuture; } @@ -135,7 +134,6 @@ private void subscribeSuccess() { if (connection != null && discoverTime > 0) { this.stopDiscoveryFuture = scheduler.schedule(() -> { this.stopDiscoveryFuture = null; - connection.unsubscribe(topicWithNode, this); connection.unsubscribe(topic, this); this.discoveredListener = null; discoverFinishedFuture.complete(null); @@ -155,7 +153,6 @@ private void subscribeSuccess() { this.discoveredListener = null; final MqttBrokerConnection connection = connectionRef.get(); if (connection != null) { - connection.unsubscribe(topicWithNode, this); connection.unsubscribe(topic, this); connectionRef.clear(); } diff --git a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/HaID.java b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/HaID.java index 08f80b7289b34..1ecaca59c4006 100644 --- a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/HaID.java +++ b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/HaID.java @@ -12,10 +12,10 @@ */ package org.openhab.binding.mqtt.generic.internal.convention.homeassistant; +import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.smarthome.core.thing.ChannelUID; -import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; -import org.openhab.binding.mqtt.generic.internal.MqttBindingConstants; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.core.Configuration; /** * HomeAssistant MQTT components use a specific MQTT topic layout, @@ -23,37 +23,51 @@ * followed by the component id, an optional node id and the object id. * * This helper class can split up an MQTT topic into such parts. + *

+ * Implementation note: This is an immutable class. * * @author David Graeff - Initial contribution */ @NonNullByDefault public class HaID { - final public String baseTopic; - final public String component; - final public String nodeID; - final public String objectID; + public final String baseTopic; + public final String component; + public final String nodeID; + public final String objectID; + + private final String topic; /** * Creates a {@link HaID} object for a given HomeAssistant MQTT topic. * * @param mqttTopic A topic like "homeassistant/binary_sensor/garden/config" or - * "homeassistant/binary_sensor/0/garden/config" + * "homeassistant/binary_sensor/0/garden/config" */ public HaID(String mqttTopic) { String[] strings = mqttTopic.split("/"); - if (strings.length < 3) { - throw new IllegalArgumentException("MQTT topic not a HomeAssistant topic!"); + if (strings.length < 4 || strings.length > 5) { + throw new IllegalArgumentException("MQTT topic not a HomeAssistant topic (wrong length)!"); + } + if (!"config".equals(strings[strings.length - 1])) { + throw new IllegalArgumentException("MQTT topic not a HomeAssistant topic ('config' missing)!"); } - if (strings.length >= 5) { - component = strings[1]; + + baseTopic = strings[0]; + component = strings[1]; + + if (strings.length == 5) { nodeID = strings[2]; objectID = strings[3]; } else { - component = strings[1]; nodeID = ""; objectID = strings[2]; } - baseTopic = strings[0]; + + this.topic = createTopic(this); + } + + public HaID() { + this("", "", "", ""); } /** @@ -64,66 +78,173 @@ public HaID(String mqttTopic) { * @param nodeID The node ID (can be the empty string) * @param component The component ID */ - public HaID(String baseTopic, String objectID, String nodeID, String component) { + private HaID(String baseTopic, String objectID, String nodeID, String component) { this.baseTopic = baseTopic; this.objectID = objectID; this.nodeID = nodeID; this.component = component; + this.topic = createTopic(this); + } + + private static final String createTopic(HaID id) { + StringBuilder str = new StringBuilder(); + str.append(id.baseTopic).append('/').append(id.component).append('/'); + if (StringUtils.isNotBlank(id.nodeID)) { + str.append(id.nodeID).append('/'); + } + str.append(id.objectID).append('/'); + return str.toString(); } /** - * Creates a {@link HaID} by providing a channel UID. + * Extract the HaID information from a channel configuration. + *

+ * objectid, nodeid, and component values are fetched from the configuration. * - * @param baseTopic The base topic. Usually "homeassistant". - * @param channel The channel UID + * @param baseTopic + * @param config + * @return newly created HaID */ - public HaID(String baseTopic, ChannelUID channel) { - String groupId = channel.getGroupId(); - if (groupId == null) { - throw new IllegalArgumentException("Channel needs a group ID!"); - } - String[] groupParts = groupId.split("_"); - if (groupParts.length != 2) { - throw new IllegalArgumentException("Channel needs a group ID with the pattern component_node!"); - } - this.objectID = channel.getThingUID().getId(); - this.nodeID = groupParts[1]; - this.component = groupParts[0]; - this.baseTopic = baseTopic; + public static HaID fromConfig(String baseTopic, Configuration config) { + String objectID = (String) config.get("objectid"); + String nodeID = (String) config.getProperties().getOrDefault("nodeid", ""); + String component = (String) config.get("component"); + return new HaID(baseTopic, objectID, nodeID, component); } /** - * We map the HomeAssistant MQTT topic tree object to an ESH Thing. + * Add the HaID information to a channel configuration. + *

+ * objectid, nodeid, and component values are added to the configuration. + * + * @param config + * @return the modified configuration */ - public String getThingID() { - return objectID; + public Configuration toConfig(Configuration config) { + config.put("objectid", objectID); + config.put("nodeid", nodeID); + config.put("component", component); + return config; } /** - * The channel group type UID consists of all components of this object (object-id + node-id + component-id). + * Extract the HaID information from a thing configuration. + *

+ * basetpoic and objectid are taken from the configuration. + * The objectid string may be in the form nodeid/objectid. + *

+ * The component component in the resulting HaID will be set to +. + * This enables the HaID to be used as an mqtt subscription topic. + * + * @param config + * @return newly created HaID */ - public String getChannelGroupTypeID() { - return objectID + "_" + component + nodeID; + public static HaID fromConfig(HandlerConfiguration config) { + String objectID = config.objectid; + String nodeID = ""; + + if (StringUtils.contains(objectID, '/')) { + String[] parts = objectID.split("/"); + + if (parts.length != 2) { + throw new IllegalArgumentException( + "Bad configuration. objectid must be or /!"); + } + nodeID = parts[0]; + objectID = parts[1]; + } + return new HaID(config.basetopic, objectID, nodeID, "+"); } /** - * A channel type UID consists of all components of this object (object-id + node-id + component-id) and a - * channel-id on top. + * Create a new thing configuration which contains the information from this HaID. + *

+ * objectid in the thing configuration will be + * nodeID/objectID from the HaID, if nodeID is not empty. + *

+ * component value will not be preserved. + * + * @return the new thing configuration */ - public ChannelTypeUID getChannelTypeID(String channelID) { - return new ChannelTypeUID(MqttBindingConstants.BINDING_ID, - objectID + "_" + component + nodeID + "_" + channelID); + public HandlerConfiguration toHandlerConfiguration() { + String objectID = this.objectID; + if (StringUtils.isNotBlank(nodeID)) { + objectID = nodeID + "/" + objectID; + } + + return new HandlerConfiguration(baseTopic, objectID); } /** - * The channel group ID consists of the node-id and the component-id + * The default group id is the unique_id of the component, given in the config-json. + * If the unique id is not set, then a fallback is constructed from the HaID information. + * + * @return fallback group id */ - public String getChannelGroupID() { - return component + "_" + nodeID; + public String getFallbackGroupId() { + StringBuilder str = new StringBuilder(); + + if (StringUtils.isNotBlank(nodeID)) { + str.append(nodeID).append('_'); + } + str.append(objectID).append('_').append(component); + return str.toString(); + } + + /** + * Return a topic, which can be used for a mqtt subscription. + * Defined values for suffix are: + *

    + *
  • config
  • + *
  • state
  • + *
+ * + * @return fallback group id + */ + public String getTopic(String suffix) { + return topic + suffix; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + baseTopic.hashCode(); + result = prime * result + component.hashCode(); + result = prime * result + nodeID.hashCode(); + result = prime * result + objectID.hashCode(); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + HaID other = (HaID) obj; + if (!baseTopic.equals(other.baseTopic)) { + return false; + } + if (!component.equals(other.component)) { + return false; + } + if (!nodeID.equals(other.nodeID)) { + return false; + } + if (!objectID.equals(other.objectID)) { + return false; + } + return true; } @Override public String toString() { - return baseTopic + "/" + component + "/" + nodeID + "/" + objectID; + return topic; } } diff --git a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/HandlerConfiguration.java b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/HandlerConfiguration.java index 5d79ad2c26653..3b11d0aa5ecaa 100644 --- a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/HandlerConfiguration.java +++ b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/convention/homeassistant/HandlerConfiguration.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.mqtt.generic.internal.convention.homeassistant; +import java.util.Map; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.mqtt.generic.internal.handler.HomeAssistantThingHandler; @@ -25,11 +27,35 @@ @NonNullByDefault public class HandlerConfiguration { /** + * hint: cannot be final, or getConfigAs will not work. * The MQTT prefix topic */ - public String basetopic = "homeassistant"; + public String basetopic; /** + * hint: cannot be final, or getConfigAs will not work. * The object id. This is comparable to a Homie Device. */ - public String objectid = ""; + public String objectid; + + public HandlerConfiguration() { + this("homeassistant", ""); + } + + public HandlerConfiguration(String basetopic, String objectid) { + super(); + this.basetopic = basetopic; + this.objectid = objectid; + } + + /** + * Add the basetopic and objectid to the properties. + * + * @param properties + * @return the modified properties + */ + public > T appendToProperties(T properties) { + properties.put("basetopic", basetopic); + properties.put("objectid", objectid); + return properties; + } } diff --git a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/discovery/HomeAssistantDiscovery.java b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/discovery/HomeAssistantDiscovery.java index a2f3badc2070d..16b1a524523d0 100644 --- a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/discovery/HomeAssistantDiscovery.java +++ b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/discovery/HomeAssistantDiscovery.java @@ -31,9 +31,10 @@ import org.eclipse.smarthome.io.transport.mqtt.MqttBrokerConnection; import org.openhab.binding.mqtt.discovery.MQTTTopicDiscoveryService; import org.openhab.binding.mqtt.generic.internal.MqttBindingConstants; -import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.ChannelConfigurationTypeAdapterFactory; import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.BaseChannelConfiguration; +import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.ChannelConfigurationTypeAdapterFactory; import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.HaID; +import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.HandlerConfiguration; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; @@ -129,9 +130,9 @@ public void receivedMessage(ThingUID connectionBridge, MqttBrokerConnection conn // We will of course find multiple of the same unique Thing IDs, for each different component another one. // Therefore the components are assembled into a list and given to the DiscoveryResult label for the user to - // easily recognise object capabilities. + // easily recognize object capabilities. HaID topicParts = determineTopicParts(topic); - final String thingID = topicParts.getThingID(); + final String thingID = topicParts.objectID; final ThingUID thingUID = new ThingUID(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, connectionBridge, thingID); @@ -155,12 +156,12 @@ public void receivedMessage(ThingUID connectionBridge, MqttBrokerConnection conn final String componentNames = components.stream().map(c -> HA_COMP_TO_NAME.getOrDefault(c, c)) .collect(Collectors.joining(",")); - BaseChannelConfiguration config = BaseChannelConfiguration.fromString(new String(payload, StandardCharsets.UTF_8), gson); + BaseChannelConfiguration config = BaseChannelConfiguration + .fromString(new String(payload, StandardCharsets.UTF_8), gson); Map properties = new HashMap<>(); - properties.put("objectid", topicParts.objectID); - properties.put("nodeid", topicParts.nodeID); - properties.put("basetopic", BASE_TOPIC); + HandlerConfiguration handlerConfig = topicParts.toHandlerConfiguration(); + properties = handlerConfig.appendToProperties(properties); config.addDeviceProperties(properties); // First remove an already discovered thing with the same ID thingRemoved(thingUID); @@ -175,7 +176,7 @@ public void topicVanished(ThingUID connectionBridge, MqttBrokerConnection connec if (!topic.endsWith("/config")) { return; } - final String thingID = determineTopicParts(topic).getThingID(); + final String thingID = determineTopicParts(topic).objectID; componentsPerThingID.remove(thingID); thingRemoved(new ThingUID(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, connectionBridge, thingID)); } diff --git a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/handler/HomeAssistantThingHandler.java b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/handler/HomeAssistantThingHandler.java index 217d03f998b4a..a0e907f6e8549 100644 --- a/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/handler/HomeAssistantThingHandler.java +++ b/addons/binding/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/internal/handler/HomeAssistantThingHandler.java @@ -22,6 +22,7 @@ import java.util.concurrent.TimeoutException; import java.util.function.Consumer; +import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Channel; @@ -29,12 +30,13 @@ import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.io.transport.mqtt.MqttBrokerConnection; import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.AbstractComponent; import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.CChannel; import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.CFactory; -import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.DiscoverComponents; import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.ChannelConfigurationTypeAdapterFactory; +import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.DiscoverComponents; import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.DiscoverComponents.ComponentDiscovered; import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.HaID; import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.HandlerConfiguration; @@ -78,7 +80,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler protected final Map> haComponents = new HashMap<>(); protected HandlerConfiguration config = new HandlerConfiguration(); - private HaID discoveryHomeAssistantID = new HaID("", "", "", ""); + private HaID discoveryHomeAssistantID = new HaID(); protected final TransformationServiceProvider transformationServiceProvider; @@ -108,11 +110,11 @@ public HomeAssistantThingHandler(Thing thing, MqttChannelTypeProvider channelTyp @Override public void initialize() { config = getConfigAs(HandlerConfiguration.class); - if (config.objectid.isEmpty()) { + if (StringUtils.isEmpty(config.objectid)) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Device ID unknown"); return; } - discoveryHomeAssistantID = new HaID(config.basetopic, config.objectid, "", ""); + discoveryHomeAssistantID = HaID.fromConfig(config); for (Channel channel : thing.getChannels()) { final String groupID = channel.getUID().getGroupId(); @@ -127,7 +129,15 @@ public void initialize() { continue; } - component = CFactory.createComponent(config.basetopic, channel, this, gson, transformationServiceProvider); + HaID haID = HaID.fromConfig(config.basetopic, channel.getConfiguration()); + ThingUID thingUID = channel.getUID().getThingUID(); + String channelConfigurationJSON = (String) channel.getConfiguration().get("config"); + if (channelConfigurationJSON == null) { + logger.warn("Provided channel does not have a 'config' configuration key!"); + } else { + component = CFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, gson, + transformationServiceProvider); + } if (component != null) { haComponents.put(component.uid().getId(), component);