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