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.nest.internal.sdm.handler;
15 import static org.openhab.core.thing.ThingStatus.*;
17 import java.time.ZonedDateTime;
18 import java.util.HashMap;
19 import java.util.List;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.stream.Collectors;
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;
63 * The {@link SDMBaseHandler} provides the common functionality of all SDM device thing handlers.
65 * @author Brian Higginbotham - Initial contribution
66 * @author Wouter Born - Initial contribution
69 public abstract class SDMBaseHandler extends BaseThingHandler implements SDMIdentifiable, SDMEventListener {
71 private final Logger logger = LoggerFactory.getLogger(SDMBaseHandler.class);
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;
80 public SDMBaseHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
82 this.timeZoneProvider = timeZoneProvider;
86 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
91 * Updates the thing state based on that of the bridge.
93 protected void updateBridgeStatus() {
94 Bridge bridge = getBridge();
95 ThingStatus bridgeStatus = bridge != null ? bridge.getStatus() : null;
98 updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
99 } else if (bridgeStatus == ONLINE && thing.getStatus() != ONLINE) {
101 } else if (bridgeStatus == OFFLINE) {
103 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
104 } else if (bridgeStatus == UNKNOWN) {
106 updateStatus(UNKNOWN);
111 public void handleCommand(ChannelUID channelUID, Command command) {
112 if (command instanceof RefreshType) {
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();
127 public void dispose() {
132 public String getId() {
136 protected @Nullable SDMAccountHandler getAccountHandler() {
137 Bridge bridge = getBridge();
138 return bridge != null ? (SDMAccountHandler) bridge.getHandler() : null;
141 protected @Nullable SDMAPI getAPI() {
142 SDMAccountHandler accountHandler = getAccountHandler();
143 return accountHandler != null ? accountHandler.getAPI() : null;
146 protected @Nullable SDMDevice getDeviceInfo() throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
147 SDMAPI api = getAPI();
148 return api == null ? null : api.getDevice(deviceId);
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);
157 protected @Nullable SDMTraits getTraitsForUpdate(SDMEvent event) {
158 SDMResourceUpdate resourceUpdate = event.resourceUpdate;
159 if (resourceUpdate == null) {
163 SDMTraits traits = resourceUpdate.traits;
164 if (traits == null) {
168 ZonedDateTime localRefreshDateTime = lastRefreshDateTime;
169 if (localRefreshDateTime == null || event.timestamp.isBefore(localRefreshDateTime)) {
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);
185 protected void refreshDevice() {
187 SDMDevice localDevice = getDeviceInfo();
188 if (localDevice == null) {
189 logger.debug("Cannot refresh device (empty response or handler has no bridge)");
193 this.device = localDevice;
194 this.lastRefreshDateTime = ZonedDateTime.now();
196 Map<String, String> properties = editProperties();
197 properties.putAll(getDeviceProperties(localDevice));
198 updateProperties(properties);
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());
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");
215 ThingStatus thingStatus = connectivity == null || connectivity.status == null
216 || connectivity.status == SDMConnectivityStatus.ONLINE ? ThingStatus.ONLINE : ThingStatus.OFFLINE;
218 if (thing.getStatus() != thingStatus) {
219 updateStatus(thingStatus);
223 protected void enableRefresh() {
224 scheduleRefreshJob();
225 SDMAccountHandler handler = getAccountHandler();
226 if (handler != null) {
227 handler.addThingDataListener(getId(), this);
231 protected void disableRefresh() {
233 SDMAccountHandler handler = getAccountHandler();
234 if (handler != null) {
235 handler.removeThingDataListener(getId(), this);
239 protected void cancelRefreshJob() {
240 ScheduledFuture<?> localRefreshJob = refreshJob;
241 if (localRefreshJob != null && !localRefreshJob.isCancelled()) {
242 localRefreshJob.cancel(true);
246 protected void scheduleRefreshJob() {
247 ScheduledFuture<?> localRefreshJob = refreshJob;
248 if (localRefreshJob == null || localRefreshJob.isCancelled()) {
249 refreshJob = scheduler.scheduleWithFixedDelay(this::refreshDevice, 0, config.refreshInterval,
254 protected void delayedRefresh() {
256 refreshJob = scheduler.scheduleWithFixedDelay(this::refreshDevice, 3, config.refreshInterval, TimeUnit.SECONDS);
259 public static Map<String, String> getDeviceProperties(SDMDevice device) {
260 Map<String, String> properties = new HashMap<>();
262 SDMTraits traits = device.traits;
264 SDMDeviceInfoTrait deviceInfo = traits.deviceInfo;
265 if (deviceInfo != null && !deviceInfo.customName.isBlank()) {
266 properties.put(SDMBindingConstants.PROPERTY_CUSTOM_NAME, deviceInfo.customName);
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);
277 SDMDeviceSettingsTrait deviceSettings = traits.deviceSettings;
278 if (deviceSettings != null) {
279 properties.put(SDMBindingConstants.PROPERTY_TEMPERATURE_SCALE, deviceSettings.temperatureScale.name());
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));
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(", ")));
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));
304 List<String> supportedProtocols = cameraLiveStream.supportedProtocols;
305 if (supportedProtocols != null) {
306 properties.put(SDMBindingConstants.PROPERTY_SUPPORTED_PROTOCOLS,
307 supportedProtocols.stream().collect(Collectors.joining(", ")));
310 List<String> videoCodecs = cameraLiveStream.videoCodecs;
311 if (videoCodecs != null) {
312 properties.put(SDMBindingConstants.PROPERTY_VIDEO_CODECS,
313 videoCodecs.stream().collect(Collectors.joining(", ")));