]> git.basschouten.com Git - openhab-addons.git/blob
28de2fd465993f855656a431e5be1f9f762a5d43
[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.nest.internal.sdm.handler;
14
15 import static org.openhab.core.thing.ThingStatus.*;
16
17 import java.time.ZonedDateTime;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.stream.Collectors;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.nest.internal.sdm.SDMBindingConstants;
28 import org.openhab.binding.nest.internal.sdm.api.SDMAPI;
29 import org.openhab.binding.nest.internal.sdm.config.SDMDeviceConfiguration;
30 import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMCommandRequest;
31 import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMCommandResponse;
32 import org.openhab.binding.nest.internal.sdm.dto.SDMDevice;
33 import org.openhab.binding.nest.internal.sdm.dto.SDMEvent;
34 import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMResourceUpdate;
35 import org.openhab.binding.nest.internal.sdm.dto.SDMIdentifiable;
36 import org.openhab.binding.nest.internal.sdm.dto.SDMParentRelation;
37 import org.openhab.binding.nest.internal.sdm.dto.SDMResourceName.SDMResourceNameType;
38 import org.openhab.binding.nest.internal.sdm.dto.SDMTraits;
39 import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMCameraImageTrait;
40 import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMCameraLiveStreamTrait;
41 import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMConnectivityStatus;
42 import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMConnectivityTrait;
43 import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMDeviceInfoTrait;
44 import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMDeviceSettingsTrait;
45 import org.openhab.binding.nest.internal.sdm.dto.SDMTraits.SDMResolution;
46 import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException;
47 import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException;
48 import org.openhab.binding.nest.internal.sdm.listener.SDMEventListener;
49 import org.openhab.core.i18n.TimeZoneProvider;
50 import org.openhab.core.thing.Bridge;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.thing.ThingStatusInfo;
56 import org.openhab.core.thing.binding.BaseThingHandler;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.RefreshType;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 /**
63  * The {@link SDMBaseHandler} provides the common functionality of all SDM device thing handlers.
64  *
65  * @author Brian Higginbotham - Initial contribution
66  * @author Wouter Born - Initial contribution
67  */
68 @NonNullByDefault
69 public abstract class SDMBaseHandler extends BaseThingHandler implements SDMIdentifiable, SDMEventListener {
70
71     private final Logger logger = LoggerFactory.getLogger(SDMBaseHandler.class);
72
73     protected @NonNullByDefault({}) SDMDeviceConfiguration config;
74     protected SDMDevice device = new SDMDevice();
75     protected String deviceId = "";
76     protected @Nullable ZonedDateTime lastRefreshDateTime;
77     protected @Nullable ScheduledFuture<?> refreshJob;
78     protected final TimeZoneProvider timeZoneProvider;
79
80     public SDMBaseHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
81         super(thing);
82         this.timeZoneProvider = timeZoneProvider;
83     }
84
85     @Override
86     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
87         updateBridgeStatus();
88     }
89
90     /**
91      * Updates the thing state based on that of the bridge.
92      */
93     protected void updateBridgeStatus() {
94         Bridge bridge = getBridge();
95         ThingStatus bridgeStatus = bridge != null ? bridge.getStatus() : null;
96         if (bridge == null) {
97             disableRefresh();
98             updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
99         } else if (bridgeStatus == ONLINE && thing.getStatus() != ONLINE) {
100             enableRefresh();
101         } else if (bridgeStatus == OFFLINE) {
102             disableRefresh();
103             updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
104         } else if (bridgeStatus == UNKNOWN) {
105             disableRefresh();
106             updateStatus(UNKNOWN);
107         }
108     }
109
110     @Override
111     public void handleCommand(ChannelUID channelUID, Command command) {
112         if (command instanceof RefreshType) {
113             delayedRefresh();
114         }
115     }
116
117     @Override
118     public void initialize() {
119         logger.debug("Initializing handler for {}", thing.getUID());
120         config = getConfigAs(SDMDeviceConfiguration.class);
121         deviceId = config.deviceId;
122         updateStatus(ThingStatus.UNKNOWN);
123         updateBridgeStatus();
124     }
125
126     @Override
127     public void dispose() {
128         disableRefresh();
129     }
130
131     @Override
132     public String getId() {
133         return deviceId;
134     }
135
136     protected @Nullable SDMAccountHandler getAccountHandler() {
137         Bridge bridge = getBridge();
138         return bridge != null ? (SDMAccountHandler) bridge.getHandler() : null;
139     }
140
141     protected @Nullable SDMAPI getAPI() {
142         SDMAccountHandler accountHandler = getAccountHandler();
143         return accountHandler != null ? accountHandler.getAPI() : null;
144     }
145
146     protected @Nullable SDMDevice getDeviceInfo() throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
147         SDMAPI api = getAPI();
148         return api == null ? null : api.getDevice(deviceId);
149     }
150
151     protected <T extends SDMCommandResponse> @Nullable T executeDeviceCommand(SDMCommandRequest<T> request)
152             throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
153         SDMAPI api = getAPI();
154         return api == null ? null : api.executeDeviceCommand(deviceId, request);
155     }
156
157     protected @Nullable SDMTraits getTraitsForUpdate(SDMEvent event) {
158         SDMResourceUpdate resourceUpdate = event.resourceUpdate;
159         if (resourceUpdate == null) {
160             return null;
161         }
162
163         SDMTraits traits = resourceUpdate.traits;
164         if (traits == null) {
165             return null;
166         }
167
168         ZonedDateTime localRefreshDateTime = lastRefreshDateTime;
169         if (localRefreshDateTime == null || event.timestamp.isBefore(localRefreshDateTime)) {
170             return null;
171         }
172
173         return traits;
174     }
175
176     @Override
177     public void onEvent(SDMEvent event) {
178         SDMTraits traits = getTraitsForUpdate(event);
179         if (traits != null) {
180             logger.debug("Updating traits using resource update traits in event");
181             device.traits.updateTraits(traits);
182         }
183     }
184
185     protected void refreshDevice() {
186         try {
187             SDMDevice localDevice = getDeviceInfo();
188             if (localDevice == null) {
189                 logger.debug("Cannot refresh device (empty response or handler has no bridge)");
190                 return;
191             }
192
193             this.device = localDevice;
194             this.lastRefreshDateTime = ZonedDateTime.now();
195
196             Map<String, String> properties = editProperties();
197             properties.putAll(getDeviceProperties(localDevice));
198             updateProperties(properties);
199
200             updateStateWithTraits(localDevice.traits);
201         } catch (InvalidSDMAccessTokenException e) {
202             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
203         } catch (FailedSendingSDMDataException e) {
204             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
205         }
206     }
207
208     protected void updateStateWithTraits(SDMTraits traits) {
209         SDMConnectivityTrait connectivity = traits.connectivity;
210         if (connectivity == null && device.traits.connectivity != null) {
211             logger.debug("Skipping partial update for device with connectivity trait");
212             return;
213         }
214
215         ThingStatus thingStatus = connectivity == null || connectivity.status == null
216                 || connectivity.status == SDMConnectivityStatus.ONLINE ? ThingStatus.ONLINE : ThingStatus.OFFLINE;
217
218         if (thing.getStatus() != thingStatus) {
219             updateStatus(thingStatus);
220         }
221     }
222
223     protected void enableRefresh() {
224         scheduleRefreshJob();
225         SDMAccountHandler handler = getAccountHandler();
226         if (handler != null) {
227             handler.addThingDataListener(getId(), this);
228         }
229     }
230
231     protected void disableRefresh() {
232         cancelRefreshJob();
233         SDMAccountHandler handler = getAccountHandler();
234         if (handler != null) {
235             handler.removeThingDataListener(getId(), this);
236         }
237     }
238
239     protected void cancelRefreshJob() {
240         ScheduledFuture<?> localRefreshJob = refreshJob;
241         if (localRefreshJob != null && !localRefreshJob.isCancelled()) {
242             localRefreshJob.cancel(true);
243         }
244     }
245
246     protected void scheduleRefreshJob() {
247         ScheduledFuture<?> localRefreshJob = refreshJob;
248         if (localRefreshJob == null || localRefreshJob.isCancelled()) {
249             refreshJob = scheduler.scheduleWithFixedDelay(this::refreshDevice, 0, config.refreshInterval,
250                     TimeUnit.SECONDS);
251         }
252     }
253
254     protected void delayedRefresh() {
255         cancelRefreshJob();
256         refreshJob = scheduler.scheduleWithFixedDelay(this::refreshDevice, 3, config.refreshInterval, TimeUnit.SECONDS);
257     }
258
259     public static Map<String, String> getDeviceProperties(SDMDevice device) {
260         Map<String, String> properties = new HashMap<>();
261
262         SDMTraits traits = device.traits;
263
264         SDMDeviceInfoTrait deviceInfo = traits.deviceInfo;
265         if (deviceInfo != null && !deviceInfo.customName.isBlank()) {
266             properties.put(SDMBindingConstants.PROPERTY_CUSTOM_NAME, deviceInfo.customName);
267         }
268
269         List<SDMParentRelation> parentRelations = device.parentRelations;
270         for (SDMParentRelation parentRelation : parentRelations) {
271             if (parentRelation.parent.type == SDMResourceNameType.ROOM && !parentRelation.displayName.isBlank()) {
272                 properties.put(SDMBindingConstants.PROPERTY_ROOM, parentRelation.displayName);
273                 break;
274             }
275         }
276
277         SDMDeviceSettingsTrait deviceSettings = traits.deviceSettings;
278         if (deviceSettings != null) {
279             properties.put(SDMBindingConstants.PROPERTY_TEMPERATURE_SCALE, deviceSettings.temperatureScale.name());
280         }
281
282         SDMCameraImageTrait cameraImage = traits.cameraImage;
283         if (cameraImage != null) {
284             SDMResolution resolution = cameraImage.maxImageResolution;
285             properties.put(SDMBindingConstants.PROPERTY_MAX_IMAGE_RESOLUTION,
286                     String.format("%sx%s", resolution.width, resolution.height));
287         }
288
289         SDMCameraLiveStreamTrait cameraLiveStream = traits.cameraLiveStream;
290         if (cameraLiveStream != null) {
291             List<String> audioCodecs = cameraLiveStream.audioCodecs;
292             if (audioCodecs != null) {
293                 properties.put(SDMBindingConstants.PROPERTY_AUDIO_CODECS,
294                         audioCodecs.stream().collect(Collectors.joining(", ")));
295             }
296
297             SDMResolution maxVideoResolution = cameraLiveStream.maxVideoResolution;
298             if (maxVideoResolution != null) {
299                 SDMResolution resolution = maxVideoResolution;
300                 properties.put(SDMBindingConstants.PROPERTY_MAX_VIDEO_RESOLUTION,
301                         String.format("%sx%s", resolution.width, resolution.height));
302             }
303
304             List<String> supportedProtocols = cameraLiveStream.supportedProtocols;
305             if (supportedProtocols != null) {
306                 properties.put(SDMBindingConstants.PROPERTY_SUPPORTED_PROTOCOLS,
307                         supportedProtocols.stream().collect(Collectors.joining(", ")));
308             }
309
310             List<String> videoCodecs = cameraLiveStream.videoCodecs;
311             if (videoCodecs != null) {
312                 properties.put(SDMBindingConstants.PROPERTY_VIDEO_CODECS,
313                         videoCodecs.stream().collect(Collectors.joining(", ")));
314             }
315         }
316
317         return properties;
318     }
319 }