]> git.basschouten.com Git - openhab-addons.git/blob
007f566c9f61b0ce0e173608ac8a263bd66e56dd
[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.chromecast.internal;
14
15 import static org.openhab.binding.chromecast.internal.ChromecastBindingConstants.*;
16 import static su.litvak.chromecast.api.v2.MediaStatus.PlayerState.*;
17
18 import java.io.IOException;
19 import java.time.Instant;
20 import java.time.ZoneId;
21 import java.time.ZonedDateTime;
22 import java.util.Collections;
23 import java.util.List;
24 import java.util.Map;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.chromecast.internal.handler.ChromecastHandler;
29 import org.openhab.core.cache.ByteArrayFileCache;
30 import org.openhab.core.io.net.http.HttpUtil;
31 import org.openhab.core.library.types.DateTimeType;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.PercentType;
35 import org.openhab.core.library.types.PlayPauseType;
36 import org.openhab.core.library.types.PointType;
37 import org.openhab.core.library.types.QuantityType;
38 import org.openhab.core.library.types.RawType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.library.unit.Units;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.types.State;
46 import org.openhab.core.types.UnDefType;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 import su.litvak.chromecast.api.v2.Application;
51 import su.litvak.chromecast.api.v2.Media;
52 import su.litvak.chromecast.api.v2.MediaStatus;
53 import su.litvak.chromecast.api.v2.Status;
54 import su.litvak.chromecast.api.v2.Volume;
55
56 /**
57  * Responsible for updating the Thing status based on messages received from a ChromeCast. This doesn't query anything -
58  * it just parses the messages and updates the Thing. Message handling/scheduling/receiving is done elsewhere.
59  * <p>
60  * This also maintains state of both volume and the appSessionId (only if we started playing media).
61  *
62  * @author Jason Holmes - Initial contribution
63  */
64 @NonNullByDefault
65 public class ChromecastStatusUpdater {
66
67     private final Logger logger = LoggerFactory.getLogger(ChromecastStatusUpdater.class);
68
69     private final Thing thing;
70     private final ChromecastHandler callback;
71     private static final ByteArrayFileCache IMAGE_CACHE = new ByteArrayFileCache("org.openhab.binding.chromecast");
72
73     private @Nullable String appSessionId;
74     private PercentType volume = PercentType.ZERO;
75
76     // Null is valid value for last duration
77     private @Nullable Double lastDuration = null;
78
79     public ChromecastStatusUpdater(Thing thing, ChromecastHandler callback) {
80         this.thing = thing;
81         this.callback = callback;
82     }
83
84     public PercentType getVolume() {
85         return volume;
86     }
87
88     public @Nullable Double getLastDuration() {
89         return lastDuration;
90     }
91
92     public @Nullable String getAppSessionId() {
93         return appSessionId;
94     }
95
96     public void setAppSessionId(String appSessionId) {
97         this.appSessionId = appSessionId;
98     }
99
100     public void processStatusUpdate(final @Nullable Status status) {
101         if (status == null) {
102             updateStatus(ThingStatus.OFFLINE);
103             updateAppStatus(null);
104             updateVolumeStatus(null);
105             return;
106         }
107
108         if (status.applications == null) {
109             this.appSessionId = null;
110         }
111
112         updateStatus(ThingStatus.ONLINE);
113         updateAppStatus(status.getRunningApp());
114         updateVolumeStatus(status.volume);
115     }
116
117     public void updateAppStatus(final @Nullable Application application) {
118         State name = UnDefType.UNDEF;
119         State id = UnDefType.UNDEF;
120         State statusText = UnDefType.UNDEF;
121         OnOffType idling = OnOffType.ON;
122
123         if (application != null) {
124             name = new StringType(application.name);
125             id = new StringType(application.id);
126             statusText = new StringType(application.statusText);
127             idling = OnOffType.from(application.isIdleScreen);
128         }
129
130         callback.updateState(CHANNEL_APP_NAME, name);
131         callback.updateState(CHANNEL_APP_ID, id);
132         callback.updateState(CHANNEL_STATUS_TEXT, statusText);
133         callback.updateState(CHANNEL_IDLING, idling);
134     }
135
136     public void updateVolumeStatus(final @Nullable Volume volume) {
137         if (volume == null) {
138             return;
139         }
140
141         PercentType value = new PercentType((int) (volume.level * 100));
142         this.volume = value;
143
144         callback.updateState(CHANNEL_VOLUME, value);
145         callback.updateState(CHANNEL_MUTE, OnOffType.from(volume.muted));
146     }
147
148     public void updateMediaStatus(final @Nullable MediaStatus mediaStatus) {
149         logger.debug("MEDIA_STATUS {}", mediaStatus);
150
151         // In-between songs? It's thinking? It's not doing anything
152         if (mediaStatus == null) {
153             callback.updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
154             callback.updateState(CHANNEL_STOP, OnOffType.ON);
155             callback.updateState(CHANNEL_CURRENT_TIME, UnDefType.UNDEF);
156             updateMediaInfoStatus(null);
157             return;
158         }
159
160         if (mediaStatus.playerState != null) {
161             switch (mediaStatus.playerState) {
162                 case IDLE:
163                     break;
164                 case PAUSED:
165                     callback.updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
166                     callback.updateState(CHANNEL_STOP, OnOffType.OFF);
167                     break;
168                 case BUFFERING:
169                 case LOADING:
170                 case PLAYING:
171                     callback.updateState(CHANNEL_CONTROL, PlayPauseType.PLAY);
172                     callback.updateState(CHANNEL_STOP, OnOffType.OFF);
173                     break;
174                 default:
175                     logger.debug("Unknown media status: {}", mediaStatus.playerState);
176                     break;
177             }
178         }
179
180         callback.updateState(CHANNEL_CURRENT_TIME, new QuantityType<>(mediaStatus.currentTime, Units.SECOND));
181
182         // If we're playing, paused or buffering but don't have any MEDIA information don't null everything out.
183         Media media = mediaStatus.media;
184         if (media == null && (mediaStatus.playerState == null || mediaStatus.playerState == PLAYING
185                 || mediaStatus.playerState == PAUSED || mediaStatus.playerState == BUFFERING)) {
186             return;
187         }
188
189         updateMediaInfoStatus(media);
190     }
191
192     private void updateMediaInfoStatus(final @Nullable Media media) {
193         State duration = UnDefType.UNDEF;
194         String metadataType = Media.MetadataType.GENERIC.name();
195         if (media != null) {
196             metadataType = media.getMetadataType().name();
197
198             lastDuration = media.duration;
199             // duration can be null when a new song is about to play.
200             if (media.duration != null) {
201                 duration = new QuantityType<>(media.duration, Units.SECOND);
202             }
203         }
204
205         callback.updateState(CHANNEL_DURATION, duration);
206         callback.updateState(CHANNEL_METADATA_TYPE, new StringType(metadataType));
207
208         updateMetadataStatus(media == null || media.metadata == null ? Collections.emptyMap() : media.metadata);
209     }
210
211     private void updateMetadataStatus(Map<String, Object> metadata) {
212         updateLocation(metadata);
213         updateImage(metadata);
214
215         thing.getChannels().stream() //
216                 .map(channel -> channel.getUID())
217                 .filter(channelUID -> METADATA_SIMPLE_CHANNELS.contains(channelUID.getId()))
218                 .forEach(channelUID -> updateChannel(channelUID, metadata));
219     }
220
221     /** Lat/lon are combined into 1 channel so we have to handle them as a special case. */
222     private void updateLocation(Map<String, Object> metadata) {
223         if (!callback.isLinked(CHANNEL_LOCATION)) {
224             return;
225         }
226
227         Double lat = (Double) metadata.get(LOCATION_METADATA_LATITUDE);
228         Double lon = (Double) metadata.get(LOCATION_METADATA_LONGITUDE);
229         if (lat == null || lon == null) {
230             callback.updateState(CHANNEL_LOCATION, UnDefType.UNDEF);
231         } else {
232             PointType pointType = new PointType(new DecimalType(lat), new DecimalType(lon));
233             callback.updateState(CHANNEL_LOCATION, pointType);
234         }
235     }
236
237     private void updateImage(Map<String, Object> metadata) {
238         if (!(callback.isLinked(CHANNEL_IMAGE) || (callback.isLinked(CHANNEL_IMAGE_SRC)))) {
239             return;
240         }
241
242         // Channel name and metadata key don't match.
243         Object imagesValue = metadata.get("images");
244         if (imagesValue == null) {
245             callback.updateState(CHANNEL_IMAGE_SRC, UnDefType.UNDEF);
246             return;
247         }
248
249         String imageSrc = null;
250         @SuppressWarnings("unchecked")
251         List<Map<String, String>> strings = (List<Map<String, String>>) imagesValue;
252         for (Map<String, String> stringMap : strings) {
253             String url = stringMap.get("url");
254             if (url != null) {
255                 imageSrc = url;
256                 break;
257             }
258         }
259
260         if (callback.isLinked(CHANNEL_IMAGE_SRC)) {
261             callback.updateState(CHANNEL_IMAGE_SRC, imageSrc == null ? UnDefType.UNDEF : new StringType(imageSrc));
262         }
263
264         if (callback.isLinked(CHANNEL_IMAGE)) {
265             State image = imageSrc == null ? UnDefType.UNDEF : downloadImageFromCache(imageSrc);
266             callback.updateState(CHANNEL_IMAGE, image == null ? UnDefType.UNDEF : image);
267         }
268     }
269
270     private @Nullable RawType downloadImage(String url) {
271         logger.debug("Trying to download the content of URL '{}'", url);
272         try {
273             RawType downloadedImage = HttpUtil.downloadImage(url);
274             if (downloadedImage == null) {
275                 logger.debug("Failed to download the content of URL '{}'", url);
276             }
277             return downloadedImage;
278         } catch (IllegalArgumentException e) {
279             // we catch this exception to avoid confusion errors in the log file
280             // see https://github.com/openhab/openhab-core/issues/2494#issuecomment-970162025
281         }
282         return null;
283     }
284
285     private @Nullable RawType downloadImageFromCache(String url) {
286         if (IMAGE_CACHE.containsKey(url)) {
287             try {
288                 byte[] bytes = IMAGE_CACHE.get(url);
289                 String contentType = HttpUtil.guessContentTypeFromData(bytes);
290                 return new RawType(bytes,
291                         contentType == null || contentType.isEmpty() ? RawType.DEFAULT_MIME_TYPE : contentType);
292             } catch (IOException e) {
293                 logger.trace("Failed to download the content of URL '{}'", url, e);
294             }
295         } else {
296             RawType image = downloadImage(url);
297             if (image != null) {
298                 IMAGE_CACHE.put(url, image.getBytes());
299                 return image;
300             }
301         }
302         return null;
303     }
304
305     private void updateChannel(ChannelUID channelUID, Map<String, Object> metadata) {
306         if (!callback.isLinked(channelUID)) {
307             return;
308         }
309
310         Object value = getValue(channelUID.getId(), metadata);
311         State state;
312
313         if (value == null) {
314             state = UnDefType.UNDEF;
315         } else if (value instanceof Double) {
316             state = new DecimalType((Double) value);
317         } else if (value instanceof Integer) {
318             state = new DecimalType(((Integer) value).longValue());
319         } else if (value instanceof String) {
320             state = new StringType(value.toString());
321         } else if (value instanceof ZonedDateTime datetime) {
322             state = new DateTimeType(datetime);
323         } else {
324             state = UnDefType.UNDEF;
325             logger.warn("Update channel {}: Unsupported value type {}", channelUID, value.getClass().getSimpleName());
326         }
327
328         callback.updateState(channelUID, state);
329     }
330
331     private @Nullable Object getValue(String channelId, @Nullable Map<String, Object> metadata) {
332         if (metadata == null) {
333             return null;
334         }
335
336         if (CHANNEL_BROADCAST_DATE.equals(channelId) || CHANNEL_RELEASE_DATE.equals(channelId)
337                 || CHANNEL_CREATION_DATE.equals(channelId)) {
338             String dateString = (String) metadata.get(channelId);
339             return (dateString == null) ? null
340                     : ZonedDateTime.ofInstant(Instant.parse(dateString), ZoneId.systemDefault());
341         }
342
343         return metadata.get(channelId);
344     }
345
346     public void updateStatus(ThingStatus status) {
347         updateStatus(status, ThingStatusDetail.NONE, null);
348     }
349
350     public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
351         callback.updateStatus(status, statusDetail, description);
352     }
353 }