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.binding.nest.internal.sdm.SDMBindingConstants.*;
16 import static org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageRequest.EVENT_IMAGE_VALIDITY;
18 import java.math.BigDecimal;
19 import java.time.Duration;
20 import java.time.ZonedDateTime;
21 import java.util.stream.Stream;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.nest.internal.sdm.SDMBindingConstants;
26 import org.openhab.binding.nest.internal.sdm.api.SDMAPI;
27 import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageRequest;
28 import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageResponse;
29 import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraImageResults;
30 import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamRequest;
31 import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamResponse;
32 import org.openhab.binding.nest.internal.sdm.dto.SDMCommands.SDMGenerateCameraRtspStreamResults;
33 import org.openhab.binding.nest.internal.sdm.dto.SDMEvent;
34 import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMDeviceEvent;
35 import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMResourceUpdate;
36 import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMResourceUpdateEvents;
37 import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException;
38 import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException;
39 import org.openhab.core.config.core.Configuration;
40 import org.openhab.core.i18n.TimeZoneProvider;
41 import org.openhab.core.library.types.DateTimeType;
42 import org.openhab.core.library.types.RawType;
43 import org.openhab.core.library.types.StringType;
44 import org.openhab.core.thing.Channel;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.types.State;
47 import org.openhab.core.types.UnDefType;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * The {@link SDMCameraHandler} handles state updates of SDM devices with a camera.
54 * @author Brian Higginbotham - Initial contribution
55 * @author Wouter Born - Initial contribution
58 public class SDMCameraHandler extends SDMBaseHandler {
60 private final Logger logger = LoggerFactory.getLogger(SDMCameraHandler.class);
62 private @Nullable ZonedDateTime lastChimeEventTimestamp;
63 private @Nullable ZonedDateTime lastMotionEventTimestamp;
64 private @Nullable ZonedDateTime lastPersonEventTimestamp;
65 private @Nullable ZonedDateTime lastSoundEventTimestamp;
67 public SDMCameraHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
68 super(thing, timeZoneProvider);
71 private void updateLiveStreamChannels() throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
72 boolean channelLinked = Stream.of(CHANNEL_LIVE_STREAM_CURRENT_TOKEN, CHANNEL_LIVE_STREAM_EXPIRATION_TIMESTAMP,
73 CHANNEL_LIVE_STREAM_EXTENSION_TOKEN, CHANNEL_LIVE_STREAM_URL).anyMatch(this::isLinked);
75 logger.debug("Not updating live stream channels (channels are not linked)");
79 logger.debug("Updating live stream channels");
81 SDMGenerateCameraRtspStreamResponse response = executeDeviceCommand(new SDMGenerateCameraRtspStreamRequest());
82 if (response == null) {
83 logger.debug("Cannot update live stream channels (empty response)");
87 SDMGenerateCameraRtspStreamResults results = response.results;
88 if (results != null) {
89 updateState(CHANNEL_LIVE_STREAM_CURRENT_TOKEN, new StringType(results.streamToken));
90 updateState(CHANNEL_LIVE_STREAM_EXPIRATION_TIMESTAMP,
91 new DateTimeType(results.expiresAt.withZoneSameInstant(timeZoneProvider.getTimeZone())));
92 updateState(CHANNEL_LIVE_STREAM_EXTENSION_TOKEN, new StringType(results.streamExtensionToken));
93 updateState(CHANNEL_LIVE_STREAM_URL, new StringType(results.streamUrls.rtspUrl));
98 public void onEvent(SDMEvent event) {
101 SDMResourceUpdate resourceUpdate = event.resourceUpdate;
102 if (resourceUpdate == null) {
103 logger.debug("Skipping event without resource update");
107 SDMResourceUpdateEvents events = resourceUpdate.events;
108 if (events == null) {
109 logger.debug("Skipping resource update without events");
114 SDMDeviceEvent deviceEvent = events.cameraMotionEvent;
115 if (deviceEvent != null) {
116 lastMotionEventTimestamp = updateImageChannelsForEvent(CHANNEL_MOTION_EVENT_TIMESTAMP,
117 CHANNEL_MOTION_EVENT_IMAGE, lastMotionEventTimestamp, event.timestamp, deviceEvent);
120 deviceEvent = events.cameraPersonEvent;
121 if (deviceEvent != null) {
122 lastPersonEventTimestamp = updateImageChannelsForEvent(CHANNEL_PERSON_EVENT_TIMESTAMP,
123 CHANNEL_PERSON_EVENT_IMAGE, lastPersonEventTimestamp, event.timestamp, deviceEvent);
126 deviceEvent = events.cameraSoundEvent;
127 if (deviceEvent != null) {
128 lastSoundEventTimestamp = updateImageChannelsForEvent(CHANNEL_SOUND_EVENT_TIMESTAMP,
129 CHANNEL_SOUND_EVENT_IMAGE, lastSoundEventTimestamp, event.timestamp, deviceEvent);
132 deviceEvent = events.doorbellChimeEvent;
133 if (deviceEvent != null) {
134 lastChimeEventTimestamp = updateImageChannelsForEvent(CHANNEL_CHIME_EVENT_TIMESTAMP,
135 CHANNEL_CHIME_EVENT_IMAGE, lastChimeEventTimestamp, event.timestamp, deviceEvent);
137 } catch (FailedSendingSDMDataException | InvalidSDMAccessTokenException e) {
138 logger.warn("Handling SDM event failed for {}", thing.getUID(), e);
142 private @Nullable ZonedDateTime updateImageChannelsForEvent(String timeChannelName, String imageChannelName,
143 @Nullable ZonedDateTime lastEventTimestamp, ZonedDateTime eventTimestamp, SDMDeviceEvent event)
144 throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
145 boolean newerEvent = lastEventTimestamp == null || lastEventTimestamp.isBefore(eventTimestamp);
147 logger.debug("Skipping {} channel update (more recent event already occurred)", imageChannelName);
148 return lastEventTimestamp;
151 if (!isLinked(imageChannelName)) {
152 logger.debug("Not downloading image for {} channel update (channel is not linked)", imageChannelName);
153 } else if (Duration.between(eventTimestamp, ZonedDateTime.now()).compareTo(EVENT_IMAGE_VALIDITY) > 0) {
154 logger.debug("Cannot download image for {} channel update (event image has expired)", imageChannelName);
155 updateState(timeChannelName, UnDefType.NULL);
157 BigDecimal imageWidth = null;
158 BigDecimal imageHeight = null;
160 Channel channel = getThing().getChannel(imageChannelName);
161 if (channel != null) {
162 Configuration configuration = channel.getConfiguration();
163 imageWidth = (BigDecimal) configuration.get(SDMBindingConstants.CONFIG_PROPERTY_IMAGE_WIDTH);
164 imageHeight = (BigDecimal) configuration.get(SDMBindingConstants.CONFIG_PROPERTY_IMAGE_HEIGHT);
167 updateState(imageChannelName, getCameraImage(event.eventId, imageWidth, imageHeight));
170 updateState(timeChannelName,
171 new DateTimeType(eventTimestamp.withZoneSameInstant(timeZoneProvider.getTimeZone())));
173 logger.debug("Updated {} channel and {} with image of event at {}", imageChannelName, timeChannelName,
176 updateLiveStreamChannels();
178 return eventTimestamp;
181 private State getCameraImage(String eventId, @Nullable BigDecimal imageWidth, @Nullable BigDecimal imageHeight)
182 throws FailedSendingSDMDataException, InvalidSDMAccessTokenException {
183 SDMGenerateCameraImageResponse response = executeDeviceCommand(new SDMGenerateCameraImageRequest(eventId));
184 if (response == null) {
185 logger.debug("Cannot get image for camera event (empty response)");
186 return UnDefType.NULL;
189 SDMGenerateCameraImageResults results = response.results;
190 if (results == null) {
191 logger.debug("Cannot get image for camera event (no results)");
192 return UnDefType.NULL;
195 SDMAPI api = getAPI();
197 logger.debug("Cannot get image for camera event (handler has no bridge)");
198 return UnDefType.NULL;
201 byte[] imageBytes = api.getCameraImage(results.url, results.token, imageWidth, imageHeight);
202 return new RawType(imageBytes, "image/jpeg");