]> git.basschouten.com Git - openhab-addons.git/commitdiff
[mqtt.homeassistant] Add support for Update component (#14241)
authorCody Cutrer <cody@cutrer.us>
Thu, 14 Dec 2023 22:53:14 +0000 (15:53 -0700)
committerGitHub <noreply@github.com>
Thu, 14 Dec 2023 22:53:14 +0000 (23:53 +0100)
* [mqtt.homeassistant] add support for Update component

This component is fairly non-standard - it doesn't add any channels.
Instead, it provides several properties to the thing, and also adds
a thing configuration allowing you to trigger an OTA update on a
Home Assistant device from MainUI.

---------

Signed-off-by: Cody Cutrer <cody@cutrer.us>
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Update.java [new file with mode: 0644]
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/config/homeassistant-thing-config.xml [new file with mode: 0644]
bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt.properties
bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/thing/homeassistant-thing.xml

index b77eeffec7e15c950d7e3d5554adf6ad1a8d1aff..c6432e1ad13a2ad866cd13812d5136372653f127 100644 (file)
@@ -104,7 +104,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {
                         gson, transformationServiceProvider);
                 component.setConfigSeen();
 
-                logger.trace("Found HomeAssistant thing {} component {}", haID.objectID, haID.component);
+                logger.trace("Found HomeAssistant component {}", haID);
 
                 if (discoveredListener != null) {
                     discoveredListener.componentDiscovered(haID, component);
index 3246c3789480c9d506c92e253d0cd6fa860a87d6..1a02711b4a3678695e65d50a21b32110873c08a8 100644 (file)
@@ -83,6 +83,8 @@ public class ComponentFactory {
                 return new Sensor(componentConfiguration);
             case "switch":
                 return new Switch(componentConfiguration);
+            case "update":
+                return new Update(componentConfiguration);
             case "vacuum":
                 return new Vacuum(componentConfiguration);
             default:
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Update.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Update.java
new file mode 100644 (file)
index 0000000..5347e7a
--- /dev/null
@@ -0,0 +1,275 @@
+/**
+ * Copyright (c) 2010-2023 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 java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
+import org.openhab.binding.mqtt.generic.values.TextValue;
+import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
+import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * A MQTT Update component, following the https://www.home-assistant.io/integrations/update.mqtt/ specification.
+ *
+ * @author Cody Cutrer - Initial contribution
+ */
+@NonNullByDefault
+public class Update extends AbstractComponent<Update.ChannelConfiguration> implements ChannelStateUpdateListener {
+    public static final String UPDATE_CHANNEL_ID = "update";
+    public static final String LATEST_VERSION_CHANNEL_ID = "latestVersion";
+
+    /**
+     * Configuration class for MQTT component
+     */
+    static class ChannelConfiguration extends AbstractChannelConfiguration {
+        ChannelConfiguration() {
+            super("MQTT Update");
+        }
+
+        @SerializedName("latest_version_template")
+        protected @Nullable String latestVersionTemplate;
+        @SerializedName("latest_version_topic")
+        protected @Nullable String latestVersionTopic;
+        @SerializedName("command_topic")
+        protected @Nullable String commandTopic;
+        @SerializedName("state_topic")
+        protected @Nullable String stateTopic;
+
+        protected @Nullable String title;
+        @SerializedName("release_summary")
+        protected @Nullable String releaseSummary;
+        @SerializedName("release_url")
+        protected @Nullable String releaseUrl;
+
+        @SerializedName("payload_install")
+        protected @Nullable String payloadInstall;
+    }
+
+    /**
+     * Describes the state payload if it's JSON
+     */
+    public static class ReleaseState {
+        // these are designed to fit in with the default property of firmwareVersion
+        public static final String PROPERTY_LATEST_VERSION = "latestFirmwareVersion";
+        public static final String PROPERTY_TITLE = "firmwareTitle";
+        public static final String PROPERTY_RELEASE_SUMMARY = "firmwareSummary";
+        public static final String PROPERTY_RELEASE_URL = "firmwareURL";
+
+        @Nullable
+        String installedVersion;
+        @Nullable
+        String latestVersion;
+        @Nullable
+        String title;
+        @Nullable
+        String releaseSummary;
+        @Nullable
+        String releaseUrl;
+        @Nullable
+        String entityPicture;
+
+        public Map<String, String> appendToProperties(Map<String, String> properties) {
+            String installedVersion = this.installedVersion;
+            if (installedVersion != null && !installedVersion.isBlank()) {
+                properties.put(Thing.PROPERTY_FIRMWARE_VERSION, installedVersion);
+            }
+            // don't remove the firmwareVersion property; it might be coming from the
+            // device as well
+
+            String latestVersion = this.latestVersion;
+            if (latestVersion != null) {
+                properties.put(PROPERTY_LATEST_VERSION, latestVersion);
+            } else {
+                properties.remove(PROPERTY_LATEST_VERSION);
+            }
+            String title = this.title;
+            if (title != null) {
+                properties.put(PROPERTY_TITLE, title);
+            } else {
+                properties.remove(title);
+            }
+            String releaseSummary = this.releaseSummary;
+            if (releaseSummary != null) {
+                properties.put(PROPERTY_RELEASE_SUMMARY, releaseSummary);
+            } else {
+                properties.remove(PROPERTY_RELEASE_SUMMARY);
+            }
+            String releaseUrl = this.releaseUrl;
+            if (releaseUrl != null) {
+                properties.put(PROPERTY_RELEASE_URL, releaseUrl);
+            } else {
+                properties.remove(PROPERTY_RELEASE_URL);
+            }
+            return properties;
+        }
+    }
+
+    public interface ReleaseStateListener {
+        void releaseStateUpdated(ReleaseState newState);
+    }
+
+    private final Logger logger = LoggerFactory.getLogger(Update.class);
+
+    private ComponentChannel updateChannel;
+    private @Nullable ComponentChannel latestVersionChannel;
+    private boolean updatable = false;
+    private ReleaseState state = new ReleaseState();
+    private @Nullable ReleaseStateListener listener = null;
+
+    public Update(ComponentFactory.ComponentConfiguration componentConfiguration) {
+        super(componentConfiguration, ChannelConfiguration.class);
+
+        TextValue value = new TextValue();
+        String commandTopic = channelConfiguration.commandTopic;
+        String payloadInstall = channelConfiguration.payloadInstall;
+
+        var builder = buildChannel(UPDATE_CHANNEL_ID, value, getName(), this);
+        if (channelConfiguration.stateTopic != null) {
+            builder.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate());
+        }
+        if (commandTopic != null && payloadInstall != null) {
+            updatable = true;
+            builder.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
+                    channelConfiguration.getQos());
+        }
+        updateChannel = builder.build(false);
+
+        if (channelConfiguration.latestVersionTopic != null) {
+            value = new TextValue();
+            latestVersionChannel = buildChannel(LATEST_VERSION_CHANNEL_ID, value, getName(), this)
+                    .stateTopic(channelConfiguration.latestVersionTopic, channelConfiguration.latestVersionTemplate)
+                    .build(false);
+        }
+
+        state.title = channelConfiguration.title;
+        state.releaseSummary = channelConfiguration.releaseSummary;
+        state.releaseUrl = channelConfiguration.releaseUrl;
+    }
+
+    /**
+     * Returns if this device can be updated
+     */
+    public boolean isUpdatable() {
+        return updatable;
+    }
+
+    /**
+     * Trigger an OTA update for this device
+     */
+    public void doUpdate() {
+        if (!updatable) {
+            return;
+        }
+        String commandTopic = channelConfiguration.commandTopic;
+        String payloadInstall = channelConfiguration.payloadInstall;
+
+        updateChannel.getState().publishValue(new StringType(payloadInstall)).handle((v, ex) -> {
+            if (ex != null) {
+                logger.debug("Failed publishing value {} to topic {}: {}", payloadInstall, commandTopic,
+                        ex.getMessage());
+            } else {
+                logger.debug("Successfully published value {} to topic {}", payloadInstall, commandTopic);
+            }
+            return null;
+        });
+    }
+
+    @Override
+    public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
+            int timeout) {
+        var updateFuture = updateChannel.start(connection, scheduler, timeout);
+        ComponentChannel latestVersionChannel = this.latestVersionChannel;
+        if (latestVersionChannel == null) {
+            return updateFuture;
+        }
+
+        var latestVersionFuture = latestVersionChannel.start(connection, scheduler, timeout);
+        return CompletableFuture.allOf(updateFuture, latestVersionFuture);
+    }
+
+    @Override
+    public CompletableFuture<@Nullable Void> stop() {
+        var updateFuture = updateChannel.stop();
+        ComponentChannel latestVersionChannel = this.latestVersionChannel;
+        if (latestVersionChannel == null) {
+            return updateFuture;
+        }
+
+        var latestVersionFuture = latestVersionChannel.stop();
+        return CompletableFuture.allOf(updateFuture, latestVersionFuture);
+    }
+
+    @Override
+    public void updateChannelState(ChannelUID channelUID, State value) {
+        switch (channelUID.getIdWithoutGroup()) {
+            case UPDATE_CHANNEL_ID:
+                String strValue = value.toString();
+                try {
+                    // check if it's JSON first
+                    @Nullable
+                    final ReleaseState releaseState = getGson().fromJson(strValue, ReleaseState.class);
+                    if (releaseState != null) {
+                        state = releaseState;
+                        notifyReleaseStateUpdated();
+                        return;
+                    }
+                } catch (JsonSyntaxException e) {
+                    // Ignore; it's just a string of installed_version
+                }
+                state.installedVersion = strValue;
+                break;
+            case LATEST_VERSION_CHANNEL_ID:
+                state.latestVersion = value.toString();
+                break;
+        }
+        notifyReleaseStateUpdated();
+    }
+
+    @Override
+    public void postChannelCommand(ChannelUID channelUID, Command value) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void triggerChannel(ChannelUID channelUID, String eventPayload) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setReleaseStateUpdateListener(ReleaseStateListener listener) {
+        this.listener = listener;
+        notifyReleaseStateUpdated();
+    }
+
+    private void notifyReleaseStateUpdated() {
+        var listener = this.listener;
+        if (listener != null) {
+            listener.releaseStateUpdated(state);
+        }
+    }
+}
index 99a14d4abd9c4fe3b55d73e121d152f1d1c72dd8..718ea19d9c9f2b0fdd3b46d06a4855985ce1a77a 100644 (file)
@@ -12,6 +12,7 @@
  */
 package org.openhab.binding.mqtt.homeassistant.internal.handler;
 
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -41,8 +42,10 @@ 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.Update;
 import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
 import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
+import org.openhab.core.config.core.validation.ConfigValidationException;
 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
 import org.openhab.core.thing.Channel;
 import org.openhab.core.thing.ChannelGroupUID;
@@ -84,7 +87,8 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
         implements ComponentDiscovered, Consumer<List<AbstractComponent<?>>> {
     public static final String AVAILABILITY_CHANNEL = "availability";
     private static final Comparator<Channel> CHANNEL_COMPARATOR_BY_UID = Comparator
-            .comparing(channel -> channel.getUID().toString());;
+            .comparing(channel -> channel.getUID().toString());
+    private static final URI UPDATABLE_CONFIG_DESCRIPTION_URI = URI.create("thing-type:mqtt:homeassistant-updatable");
 
     private final Logger logger = LoggerFactory.getLogger(HomeAssistantThingHandler.class);
 
@@ -102,6 +106,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
     protected final TransformationServiceProvider transformationServiceProvider;
 
     private boolean started;
+    private @Nullable Update updateComponent;
 
     /**
      * Create a new thing handler for HomeAssistant MQTT components.
@@ -293,6 +298,11 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
                     return null;
                 });
 
+                if (discovered instanceof Update) {
+                    updateComponent = (Update) discovered;
+                    updateComponent.setReleaseStateUpdateListener(this::releaseStateUpdated);
+                }
+
                 List<Channel> discoveredChannels = discovered.getChannelMap().values().stream()
                         .map(ComponentChannel::getChannel).collect(Collectors.toList());
                 if (known != null) {
@@ -342,6 +352,26 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
         }
     }
 
+    @Override
+    public void handleConfigurationUpdate(Map<String, Object> configurationParameters)
+            throws ConfigValidationException {
+        if (configurationParameters.containsKey("doUpdate")) {
+            configurationParameters = new HashMap<>(configurationParameters);
+            Object value = configurationParameters.remove("doUpdate");
+            if (value instanceof Boolean doUpdate && doUpdate) {
+                Update updateComponent = this.updateComponent;
+                if (updateComponent == null) {
+                    logger.warn(
+                            "Received update command for Home Assistant device {}, but it does not have an update component.",
+                            getThing().getUID());
+                } else {
+                    updateComponent.doUpdate();
+                }
+            }
+        }
+        super.handleConfigurationUpdate(configurationParameters);
+    }
+
     private void updateThingType() {
         // if this is a dynamic type, then we update the type
         ThingTypeUID typeID = thing.getThingTypeUID();
@@ -354,10 +384,21 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
                 channelDefs = haComponents.values().stream().map(AbstractComponent::getChannels).flatMap(List::stream)
                         .collect(Collectors.toList());
             }
-            ThingType thingType = channelTypeProvider.derive(typeID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING)
-                    .withChannelDefinitions(channelDefs).withChannelGroupDefinitions(groupDefs).build();
+            var builder = channelTypeProvider.derive(typeID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING)
+                    .withChannelDefinitions(channelDefs).withChannelGroupDefinitions(groupDefs);
+            Update updateComponent = this.updateComponent;
+            if (updateComponent != null && updateComponent.isUpdatable()) {
+                builder.withConfigDescriptionURI(UPDATABLE_CONFIG_DESCRIPTION_URI);
+            }
+            ThingType thingType = builder.build();
 
             channelTypeProvider.setThingType(typeID, thingType);
         }
     }
+
+    private void releaseStateUpdated(Update.ReleaseState state) {
+        Map<String, String> properties = editProperties();
+        properties = state.appendToProperties(properties);
+        updateProperties(properties);
+    }
 }
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/config/homeassistant-thing-config.xml b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/config/homeassistant-thing-config.xml
new file mode 100644 (file)
index 0000000..f523cbf
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:mqtt:homeassistant">
+               <parameter name="topics" type="text" required="true" multiple="true">
+                       <label>MQTT Config Topic</label>
+                       <description>List of HomeAssistant configuration topics (e.g. /homeassistant/switch/4711/config)</description>
+               </parameter>
+
+               <parameter name="basetopic" type="text" required="true">
+                       <label>MQTT Base Prefix</label>
+                       <description>MQTT base prefix</description>
+                       <default>homeassistant</default>
+               </parameter>
+       </config-description>
+
+       <config-description uri="thing-type:mqtt:homeassistant-updatable">
+               <parameter-group name="actions">
+                       <label>Actions</label>
+               </parameter-group>
+
+               <parameter name="topics" type="text" required="true" multiple="true">
+                       <label>MQTT Config Topic</label>
+                       <description>List of HomeAssistant configuration topics (e.g. /homeassistant/switch/4711/config)</description>
+               </parameter>
+
+               <parameter name="basetopic" type="text" required="true">
+                       <label>MQTT Base Prefix</label>
+                       <description>MQTT base prefix</description>
+                       <default>homeassistant</default>
+               </parameter>
+
+               <parameter name="doUpdate" type="boolean" groupName="actions">
+                       <label>Update</label>
+                       <description>Request the device do an OTA update</description>
+                       <advanced>true</advanced>
+                       <default>false</default>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
index 2095eecad3be877135bcb9ab4242e27fe7414c6a..8fabdeed27c18bbc489a43f7703a520e4a1f1142 100644 (file)
@@ -9,6 +9,12 @@ thing-type.config.mqtt.homeassistant.basetopic.label = MQTT Base Prefix
 thing-type.config.mqtt.homeassistant.basetopic.description = MQTT base prefix
 thing-type.config.mqtt.homeassistant.topics.label = MQTT Config Topic
 thing-type.config.mqtt.homeassistant.topics.description = List of HomeAssistant configuration topics (e.g. /homeassistant/switch/4711/config)
+thing-type.config.mqtt.homeassistant-updatable.basetopic.label = MQTT Base Prefix
+thing-type.config.mqtt.homeassistant-updatable.basetopic.description = MQTT base prefix
+thing-type.config.mqtt.homeassistant-updatable.topics.label = MQTT Config Topic
+thing-type.config.mqtt.homeassistant-updatable.topics.description = List of HomeAssistant configuration topics (e.g. /homeassistant/switch/4711/config)
+thing-type.config.mqtt.homeassistant-updatable.doUpdate.label = Update
+thing-type.config.mqtt.homeassistant-updatable.doUpdate.description = Request the device do an OTA update
 
 # channel types config
 
index 51e9957a01df9a8787a57395d47202f29d2a7c7c..7af89b8ac0fdbf959564e2fc3af98e53f4e50f50 100644 (file)
                <label>HomeAssistant MQTT Component</label>
                <description>You need a configured Broker first. This Thing represents a device, that follows the "HomeAssistant MQTT
                        Component" specification.</description>
-               <config-description>
-                       <parameter name="topics" type="text" required="true" multiple="true">
-                               <label>MQTT Config Topic</label>
-                               <description>List of HomeAssistant configuration topics (e.g. /homeassistant/switch/4711/config)</description>
-                       </parameter>
-
-                       <parameter name="basetopic" type="text" required="true">
-                               <label>MQTT Base Prefix</label>
-                               <description>MQTT base prefix</description>
-                               <default>homeassistant</default>
-                       </parameter>
-               </config-description>
+               <config-description-ref uri="thing-type:mqtt:homeassistant"/>
        </thing-type>
 </thing:thing-descriptions>