]> git.basschouten.com Git - openhab-addons.git/blob
5ea64b4a24663a6dd73e2d58b097c5a5a7928af7
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mqtt.homeassistant.internal.component;
14
15 import java.util.Map;
16 import java.util.concurrent.CompletableFuture;
17 import java.util.concurrent.ScheduledExecutorService;
18
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;
34
35 import com.google.gson.JsonSyntaxException;
36 import com.google.gson.annotations.SerializedName;
37
38 /**
39  * A MQTT Update component, following the https://www.home-assistant.io/integrations/update.mqtt/ specification.
40  *
41  * @author Cody Cutrer - Initial contribution
42  */
43 @NonNullByDefault
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";
47
48     /**
49      * Configuration class for MQTT component
50      */
51     static class ChannelConfiguration extends AbstractChannelConfiguration {
52         ChannelConfiguration() {
53             super("MQTT Update");
54         }
55
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;
64
65         protected @Nullable String title;
66         @SerializedName("release_summary")
67         protected @Nullable String releaseSummary;
68         @SerializedName("release_url")
69         protected @Nullable String releaseUrl;
70
71         @SerializedName("payload_install")
72         protected @Nullable String payloadInstall;
73     }
74
75     /**
76      * Describes the state payload if it's JSON
77      */
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";
84
85         @Nullable
86         String installedVersion;
87         @Nullable
88         String latestVersion;
89         @Nullable
90         String title;
91         @Nullable
92         String releaseSummary;
93         @Nullable
94         String releaseUrl;
95         @Nullable
96         String entityPicture;
97
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);
102             }
103             // don't remove the firmwareVersion property; it might be coming from the
104             // device as well
105
106             String latestVersion = this.latestVersion;
107             if (latestVersion != null) {
108                 properties.put(PROPERTY_LATEST_VERSION, latestVersion);
109             } else {
110                 properties.remove(PROPERTY_LATEST_VERSION);
111             }
112             String title = this.title;
113             if (title != null) {
114                 properties.put(PROPERTY_TITLE, title);
115             } else {
116                 properties.remove(title);
117             }
118             String releaseSummary = this.releaseSummary;
119             if (releaseSummary != null) {
120                 properties.put(PROPERTY_RELEASE_SUMMARY, releaseSummary);
121             } else {
122                 properties.remove(PROPERTY_RELEASE_SUMMARY);
123             }
124             String releaseUrl = this.releaseUrl;
125             if (releaseUrl != null) {
126                 properties.put(PROPERTY_RELEASE_URL, releaseUrl);
127             } else {
128                 properties.remove(PROPERTY_RELEASE_URL);
129             }
130             return properties;
131         }
132     }
133
134     public interface ReleaseStateListener {
135         void releaseStateUpdated(ReleaseState newState);
136     }
137
138     private final Logger logger = LoggerFactory.getLogger(Update.class);
139
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;
145
146     public Update(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
147         super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
148
149         TextValue value = new TextValue();
150         String commandTopic = channelConfiguration.commandTopic;
151         String payloadInstall = channelConfiguration.payloadInstall;
152
153         var builder = buildChannel(UPDATE_CHANNEL_ID, ComponentChannelType.STRING, value, getName(), this);
154         if (channelConfiguration.stateTopic != null) {
155             builder.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate());
156         }
157         if (commandTopic != null && payloadInstall != null) {
158             updatable = true;
159             builder.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
160                     channelConfiguration.getQos());
161         }
162         updateChannel = builder.build(false);
163
164         if (channelConfiguration.latestVersionTopic != null) {
165             value = new TextValue();
166             latestVersionChannel = buildChannel(LATEST_VERSION_CHANNEL_ID, ComponentChannelType.STRING, value,
167                     getName(), this)
168                     .stateTopic(channelConfiguration.latestVersionTopic, channelConfiguration.latestVersionTemplate)
169                     .build(false);
170         }
171
172         state.title = channelConfiguration.title;
173         state.releaseSummary = channelConfiguration.releaseSummary;
174         state.releaseUrl = channelConfiguration.releaseUrl;
175     }
176
177     /**
178      * Returns if this device can be updated
179      */
180     public boolean isUpdatable() {
181         return updatable;
182     }
183
184     /**
185      * Trigger an OTA update for this device
186      */
187     public void doUpdate() {
188         if (!updatable) {
189             return;
190         }
191         String commandTopic = channelConfiguration.commandTopic;
192         String payloadInstall = channelConfiguration.payloadInstall;
193
194         updateChannel.getState().publishValue(new StringType(payloadInstall)).handle((v, ex) -> {
195             if (ex != null) {
196                 logger.debug("Failed publishing value {} to topic {}: {}", payloadInstall, commandTopic,
197                         ex.getMessage());
198             } else {
199                 logger.debug("Successfully published value {} to topic {}", payloadInstall, commandTopic);
200             }
201             return null;
202         });
203     }
204
205     @Override
206     public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
207             int timeout) {
208         var updateFuture = updateChannel.start(connection, scheduler, timeout);
209         ComponentChannel latestVersionChannel = this.latestVersionChannel;
210         if (latestVersionChannel == null) {
211             return updateFuture;
212         }
213
214         var latestVersionFuture = latestVersionChannel.start(connection, scheduler, timeout);
215         return CompletableFuture.allOf(updateFuture, latestVersionFuture);
216     }
217
218     @Override
219     public CompletableFuture<@Nullable Void> stop() {
220         var updateFuture = updateChannel.stop();
221         ComponentChannel latestVersionChannel = this.latestVersionChannel;
222         if (latestVersionChannel == null) {
223             return updateFuture;
224         }
225
226         var latestVersionFuture = latestVersionChannel.stop();
227         return CompletableFuture.allOf(updateFuture, latestVersionFuture);
228     }
229
230     @Override
231     public void updateChannelState(ChannelUID channelUID, State value) {
232         switch (channelUID.getIdWithoutGroup()) {
233             case UPDATE_CHANNEL_ID:
234                 String strValue = value.toString();
235                 try {
236                     // check if it's JSON first
237                     @Nullable
238                     final ReleaseState releaseState = getGson().fromJson(strValue, ReleaseState.class);
239                     if (releaseState != null) {
240                         state = releaseState;
241                         notifyReleaseStateUpdated();
242                         return;
243                     }
244                 } catch (JsonSyntaxException e) {
245                     // Ignore; it's just a string of installed_version
246                 }
247                 state.installedVersion = strValue;
248                 break;
249             case LATEST_VERSION_CHANNEL_ID:
250                 state.latestVersion = value.toString();
251                 break;
252         }
253         notifyReleaseStateUpdated();
254     }
255
256     @Override
257     public void postChannelCommand(ChannelUID channelUID, Command value) {
258         throw new UnsupportedOperationException();
259     }
260
261     @Override
262     public void triggerChannel(ChannelUID channelUID, String eventPayload) {
263         throw new UnsupportedOperationException();
264     }
265
266     public void setReleaseStateUpdateListener(ReleaseStateListener listener) {
267         this.listener = listener;
268         notifyReleaseStateUpdated();
269     }
270
271     private void notifyReleaseStateUpdated() {
272         var listener = this.listener;
273         if (listener != null) {
274             listener.releaseStateUpdated(state);
275         }
276     }
277 }