2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mqtt.homeassistant.internal.component;
16 import java.util.concurrent.CompletableFuture;
17 import java.util.concurrent.ScheduledExecutorService;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
22 import org.openhab.binding.mqtt.generic.values.TextValue;
23 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
24 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
25 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
26 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
27 import org.openhab.core.library.types.StringType;
28 import org.openhab.core.thing.ChannelUID;
29 import org.openhab.core.thing.Thing;
30 import org.openhab.core.types.Command;
31 import org.openhab.core.types.State;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
35 import com.google.gson.JsonSyntaxException;
36 import com.google.gson.annotations.SerializedName;
39 * A MQTT Update component, following the https://www.home-assistant.io/integrations/update.mqtt/ specification.
41 * @author Cody Cutrer - Initial contribution
44 public class Update extends AbstractComponent<Update.ChannelConfiguration> implements ChannelStateUpdateListener {
45 public static final String UPDATE_CHANNEL_ID = "update";
46 public static final String LATEST_VERSION_CHANNEL_ID = "latestVersion";
49 * Configuration class for MQTT component
51 static class ChannelConfiguration extends AbstractChannelConfiguration {
52 ChannelConfiguration() {
56 @SerializedName("latest_version_template")
57 protected @Nullable String latestVersionTemplate;
58 @SerializedName("latest_version_topic")
59 protected @Nullable String latestVersionTopic;
60 @SerializedName("command_topic")
61 protected @Nullable String commandTopic;
62 @SerializedName("state_topic")
63 protected @Nullable String stateTopic;
65 protected @Nullable String title;
66 @SerializedName("release_summary")
67 protected @Nullable String releaseSummary;
68 @SerializedName("release_url")
69 protected @Nullable String releaseUrl;
71 @SerializedName("payload_install")
72 protected @Nullable String payloadInstall;
76 * Describes the state payload if it's JSON
78 public static class ReleaseState {
79 // these are designed to fit in with the default property of firmwareVersion
80 public static final String PROPERTY_LATEST_VERSION = "latestFirmwareVersion";
81 public static final String PROPERTY_TITLE = "firmwareTitle";
82 public static final String PROPERTY_RELEASE_SUMMARY = "firmwareSummary";
83 public static final String PROPERTY_RELEASE_URL = "firmwareURL";
86 String installedVersion;
92 String releaseSummary;
98 public Map<String, String> appendToProperties(Map<String, String> properties) {
99 String installedVersion = this.installedVersion;
100 if (installedVersion != null && !installedVersion.isBlank()) {
101 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, installedVersion);
103 // don't remove the firmwareVersion property; it might be coming from the
106 String latestVersion = this.latestVersion;
107 if (latestVersion != null) {
108 properties.put(PROPERTY_LATEST_VERSION, latestVersion);
110 properties.remove(PROPERTY_LATEST_VERSION);
112 String title = this.title;
114 properties.put(PROPERTY_TITLE, title);
116 properties.remove(title);
118 String releaseSummary = this.releaseSummary;
119 if (releaseSummary != null) {
120 properties.put(PROPERTY_RELEASE_SUMMARY, releaseSummary);
122 properties.remove(PROPERTY_RELEASE_SUMMARY);
124 String releaseUrl = this.releaseUrl;
125 if (releaseUrl != null) {
126 properties.put(PROPERTY_RELEASE_URL, releaseUrl);
128 properties.remove(PROPERTY_RELEASE_URL);
134 public interface ReleaseStateListener {
135 void releaseStateUpdated(ReleaseState newState);
138 private final Logger logger = LoggerFactory.getLogger(Update.class);
140 private ComponentChannel updateChannel;
141 private @Nullable ComponentChannel latestVersionChannel;
142 private boolean updatable = false;
143 private ReleaseState state = new ReleaseState();
144 private @Nullable ReleaseStateListener listener = null;
146 public Update(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
147 super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
149 TextValue value = new TextValue();
150 String commandTopic = channelConfiguration.commandTopic;
151 String payloadInstall = channelConfiguration.payloadInstall;
153 var builder = buildChannel(UPDATE_CHANNEL_ID, ComponentChannelType.STRING, value, getName(), this);
154 if (channelConfiguration.stateTopic != null) {
155 builder.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate());
157 if (commandTopic != null && payloadInstall != null) {
159 builder.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
160 channelConfiguration.getQos());
162 updateChannel = builder.build(false);
164 if (channelConfiguration.latestVersionTopic != null) {
165 value = new TextValue();
166 latestVersionChannel = buildChannel(LATEST_VERSION_CHANNEL_ID, ComponentChannelType.STRING, value,
168 .stateTopic(channelConfiguration.latestVersionTopic, channelConfiguration.latestVersionTemplate)
172 state.title = channelConfiguration.title;
173 state.releaseSummary = channelConfiguration.releaseSummary;
174 state.releaseUrl = channelConfiguration.releaseUrl;
178 * Returns if this device can be updated
180 public boolean isUpdatable() {
185 * Trigger an OTA update for this device
187 public void doUpdate() {
191 String commandTopic = channelConfiguration.commandTopic;
192 String payloadInstall = channelConfiguration.payloadInstall;
194 updateChannel.getState().publishValue(new StringType(payloadInstall)).handle((v, ex) -> {
196 logger.debug("Failed publishing value {} to topic {}: {}", payloadInstall, commandTopic,
199 logger.debug("Successfully published value {} to topic {}", payloadInstall, commandTopic);
206 public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
208 var updateFuture = updateChannel.start(connection, scheduler, timeout);
209 ComponentChannel latestVersionChannel = this.latestVersionChannel;
210 if (latestVersionChannel == null) {
214 var latestVersionFuture = latestVersionChannel.start(connection, scheduler, timeout);
215 return CompletableFuture.allOf(updateFuture, latestVersionFuture);
219 public CompletableFuture<@Nullable Void> stop() {
220 var updateFuture = updateChannel.stop();
221 ComponentChannel latestVersionChannel = this.latestVersionChannel;
222 if (latestVersionChannel == null) {
226 var latestVersionFuture = latestVersionChannel.stop();
227 return CompletableFuture.allOf(updateFuture, latestVersionFuture);
231 public void updateChannelState(ChannelUID channelUID, State value) {
232 switch (channelUID.getIdWithoutGroup()) {
233 case UPDATE_CHANNEL_ID:
234 String strValue = value.toString();
236 // check if it's JSON first
238 final ReleaseState releaseState = getGson().fromJson(strValue, ReleaseState.class);
239 if (releaseState != null) {
240 state = releaseState;
241 notifyReleaseStateUpdated();
244 } catch (JsonSyntaxException e) {
245 // Ignore; it's just a string of installed_version
247 state.installedVersion = strValue;
249 case LATEST_VERSION_CHANNEL_ID:
250 state.latestVersion = value.toString();
253 notifyReleaseStateUpdated();
257 public void postChannelCommand(ChannelUID channelUID, Command value) {
258 throw new UnsupportedOperationException();
262 public void triggerChannel(ChannelUID channelUID, String eventPayload) {
263 throw new UnsupportedOperationException();
266 public void setReleaseStateUpdateListener(ReleaseStateListener listener) {
267 this.listener = listener;
268 notifyReleaseStateUpdated();
271 private void notifyReleaseStateUpdated() {
272 var listener = this.listener;
273 if (listener != null) {
274 listener.releaseStateUpdated(state);