2 * Copyright (c) 2010-2023 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.config.dto.AbstractChannelConfiguration;
25 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
26 import org.openhab.core.library.types.StringType;
27 import org.openhab.core.thing.ChannelUID;
28 import org.openhab.core.thing.Thing;
29 import org.openhab.core.types.Command;
30 import org.openhab.core.types.State;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
34 import com.google.gson.JsonSyntaxException;
35 import com.google.gson.annotations.SerializedName;
38 * A MQTT Update component, following the https://www.home-assistant.io/integrations/update.mqtt/ specification.
40 * @author Cody Cutrer - Initial contribution
43 public class Update extends AbstractComponent<Update.ChannelConfiguration> implements ChannelStateUpdateListener {
44 public static final String UPDATE_CHANNEL_ID = "update";
45 public static final String LATEST_VERSION_CHANNEL_ID = "latestVersion";
48 * Configuration class for MQTT component
50 static class ChannelConfiguration extends AbstractChannelConfiguration {
51 ChannelConfiguration() {
55 @SerializedName("latest_version_template")
56 protected @Nullable String latestVersionTemplate;
57 @SerializedName("latest_version_topic")
58 protected @Nullable String latestVersionTopic;
59 @SerializedName("command_topic")
60 protected @Nullable String commandTopic;
61 @SerializedName("state_topic")
62 protected @Nullable String stateTopic;
64 protected @Nullable String title;
65 @SerializedName("release_summary")
66 protected @Nullable String releaseSummary;
67 @SerializedName("release_url")
68 protected @Nullable String releaseUrl;
70 @SerializedName("payload_install")
71 protected @Nullable String payloadInstall;
75 * Describes the state payload if it's JSON
77 public static class ReleaseState {
78 // these are designed to fit in with the default property of firmwareVersion
79 public static final String PROPERTY_LATEST_VERSION = "latestFirmwareVersion";
80 public static final String PROPERTY_TITLE = "firmwareTitle";
81 public static final String PROPERTY_RELEASE_SUMMARY = "firmwareSummary";
82 public static final String PROPERTY_RELEASE_URL = "firmwareURL";
85 String installedVersion;
91 String releaseSummary;
97 public Map<String, String> appendToProperties(Map<String, String> properties) {
98 String installedVersion = this.installedVersion;
99 if (installedVersion != null && !installedVersion.isBlank()) {
100 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, installedVersion);
102 // don't remove the firmwareVersion property; it might be coming from the
105 String latestVersion = this.latestVersion;
106 if (latestVersion != null) {
107 properties.put(PROPERTY_LATEST_VERSION, latestVersion);
109 properties.remove(PROPERTY_LATEST_VERSION);
111 String title = this.title;
113 properties.put(PROPERTY_TITLE, title);
115 properties.remove(title);
117 String releaseSummary = this.releaseSummary;
118 if (releaseSummary != null) {
119 properties.put(PROPERTY_RELEASE_SUMMARY, releaseSummary);
121 properties.remove(PROPERTY_RELEASE_SUMMARY);
123 String releaseUrl = this.releaseUrl;
124 if (releaseUrl != null) {
125 properties.put(PROPERTY_RELEASE_URL, releaseUrl);
127 properties.remove(PROPERTY_RELEASE_URL);
133 public interface ReleaseStateListener {
134 void releaseStateUpdated(ReleaseState newState);
137 private final Logger logger = LoggerFactory.getLogger(Update.class);
139 private ComponentChannel updateChannel;
140 private @Nullable ComponentChannel latestVersionChannel;
141 private boolean updatable = false;
142 private ReleaseState state = new ReleaseState();
143 private @Nullable ReleaseStateListener listener = null;
145 public Update(ComponentFactory.ComponentConfiguration componentConfiguration) {
146 super(componentConfiguration, ChannelConfiguration.class);
148 TextValue value = new TextValue();
149 String commandTopic = channelConfiguration.commandTopic;
150 String payloadInstall = channelConfiguration.payloadInstall;
152 var builder = buildChannel(UPDATE_CHANNEL_ID, value, getName(), this);
153 if (channelConfiguration.stateTopic != null) {
154 builder.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate());
156 if (commandTopic != null && payloadInstall != null) {
158 builder.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
159 channelConfiguration.getQos());
161 updateChannel = builder.build(false);
163 if (channelConfiguration.latestVersionTopic != null) {
164 value = new TextValue();
165 latestVersionChannel = buildChannel(LATEST_VERSION_CHANNEL_ID, value, getName(), this)
166 .stateTopic(channelConfiguration.latestVersionTopic, channelConfiguration.latestVersionTemplate)
170 state.title = channelConfiguration.title;
171 state.releaseSummary = channelConfiguration.releaseSummary;
172 state.releaseUrl = channelConfiguration.releaseUrl;
176 * Returns if this device can be updated
178 public boolean isUpdatable() {
183 * Trigger an OTA update for this device
185 public void doUpdate() {
189 String commandTopic = channelConfiguration.commandTopic;
190 String payloadInstall = channelConfiguration.payloadInstall;
192 updateChannel.getState().publishValue(new StringType(payloadInstall)).handle((v, ex) -> {
194 logger.debug("Failed publishing value {} to topic {}: {}", payloadInstall, commandTopic,
197 logger.debug("Successfully published value {} to topic {}", payloadInstall, commandTopic);
204 public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
206 var updateFuture = updateChannel.start(connection, scheduler, timeout);
207 ComponentChannel latestVersionChannel = this.latestVersionChannel;
208 if (latestVersionChannel == null) {
212 var latestVersionFuture = latestVersionChannel.start(connection, scheduler, timeout);
213 return CompletableFuture.allOf(updateFuture, latestVersionFuture);
217 public CompletableFuture<@Nullable Void> stop() {
218 var updateFuture = updateChannel.stop();
219 ComponentChannel latestVersionChannel = this.latestVersionChannel;
220 if (latestVersionChannel == null) {
224 var latestVersionFuture = latestVersionChannel.stop();
225 return CompletableFuture.allOf(updateFuture, latestVersionFuture);
229 public void updateChannelState(ChannelUID channelUID, State value) {
230 switch (channelUID.getIdWithoutGroup()) {
231 case UPDATE_CHANNEL_ID:
232 String strValue = value.toString();
234 // check if it's JSON first
236 final ReleaseState releaseState = getGson().fromJson(strValue, ReleaseState.class);
237 if (releaseState != null) {
238 state = releaseState;
239 notifyReleaseStateUpdated();
242 } catch (JsonSyntaxException e) {
243 // Ignore; it's just a string of installed_version
245 state.installedVersion = strValue;
247 case LATEST_VERSION_CHANNEL_ID:
248 state.latestVersion = value.toString();
251 notifyReleaseStateUpdated();
255 public void postChannelCommand(ChannelUID channelUID, Command value) {
256 throw new UnsupportedOperationException();
260 public void triggerChannel(ChannelUID channelUID, String eventPayload) {
261 throw new UnsupportedOperationException();
264 public void setReleaseStateUpdateListener(ReleaseStateListener listener) {
265 this.listener = listener;
266 notifyReleaseStateUpdated();
269 private void notifyReleaseStateUpdated() {
270 var listener = this.listener;
271 if (listener != null) {
272 listener.releaseStateUpdated(state);