]> git.basschouten.com Git - openhab-addons.git/blob
a293e50538ef1b1cb08539a9468792ef56cde689
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.binding.chromecast.internal.utils.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.SmartHomeUnits;
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     private final Logger logger = LoggerFactory.getLogger(ChromecastStatusUpdater.class);
67
68     private final Thing thing;
69     private final ChromecastHandler callback;
70     private static final ByteArrayFileCache IMAGE_CACHE = new ByteArrayFileCache("org.openhab.binding.chromecast");
71
72     private @Nullable String appSessionId;
73     private PercentType volume = PercentType.ZERO;
74
75     public ChromecastStatusUpdater(Thing thing, ChromecastHandler callback) {
76         this.thing = thing;
77         this.callback = callback;
78     }
79
80     public PercentType getVolume() {
81         return volume;
82     }
83
84     public @Nullable String getAppSessionId() {
85         return appSessionId;
86     }
87
88     public void setAppSessionId(String appSessionId) {
89         this.appSessionId = appSessionId;
90     }
91
92     public void processStatusUpdate(final @Nullable Status status) {
93         if (status == null) {
94             updateStatus(ThingStatus.OFFLINE);
95             updateAppStatus(null);
96             updateVolumeStatus(null);
97             return;
98         }
99
100         if (status.applications == null) {
101             this.appSessionId = null;
102         }
103
104         updateStatus(ThingStatus.ONLINE);
105         updateAppStatus(status.getRunningApp());
106         updateVolumeStatus(status.volume);
107     }
108
109     public void updateAppStatus(final @Nullable Application application) {
110         State name = UnDefType.UNDEF;
111         State id = UnDefType.UNDEF;
112         State statusText = UnDefType.UNDEF;
113         OnOffType idling = OnOffType.ON;
114
115         if (application != null) {
116             name = new StringType(application.name);
117             id = new StringType(application.id);
118             statusText = new StringType(application.statusText);
119             idling = application.isIdleScreen ? OnOffType.ON : OnOffType.OFF;
120         }
121
122         callback.updateState(CHANNEL_APP_NAME, name);
123         callback.updateState(CHANNEL_APP_ID, id);
124         callback.updateState(CHANNEL_STATUS_TEXT, statusText);
125         callback.updateState(CHANNEL_IDLING, idling);
126     }
127
128     public void updateVolumeStatus(final @Nullable Volume volume) {
129         if (volume == null) {
130             return;
131         }
132
133         PercentType value = new PercentType((int) (volume.level * 100));
134         this.volume = value;
135
136         callback.updateState(CHANNEL_VOLUME, value);
137         callback.updateState(CHANNEL_MUTE, volume.muted ? OnOffType.ON : OnOffType.OFF);
138     }
139
140     public void updateMediaStatus(final @Nullable MediaStatus mediaStatus) {
141         logger.debug("MEDIA_STATUS {}", mediaStatus);
142
143         // In-between songs? It's thinking? It's not doing anything
144         if (mediaStatus == null) {
145             callback.updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
146             callback.updateState(CHANNEL_STOP, OnOffType.ON);
147             callback.updateState(CHANNEL_CURRENT_TIME, UnDefType.UNDEF);
148             updateMediaInfoStatus(null);
149             return;
150         }
151
152         switch (mediaStatus.playerState) {
153             case IDLE:
154                 break;
155             case PAUSED:
156                 callback.updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
157                 callback.updateState(CHANNEL_STOP, OnOffType.OFF);
158                 break;
159             case BUFFERING:
160             case LOADING:
161             case PLAYING:
162                 callback.updateState(CHANNEL_CONTROL, PlayPauseType.PLAY);
163                 callback.updateState(CHANNEL_STOP, OnOffType.OFF);
164                 break;
165             default:
166                 logger.debug("Unknown media status: {}", mediaStatus.playerState);
167                 break;
168         }
169
170         callback.updateState(CHANNEL_CURRENT_TIME, new QuantityType<>(mediaStatus.currentTime, SmartHomeUnits.SECOND));
171
172         // If we're playing, paused or buffering but don't have any MEDIA information don't null everything out.
173         Media media = mediaStatus.media;
174         if (media == null && (mediaStatus.playerState == PLAYING || mediaStatus.playerState == PAUSED
175                 || mediaStatus.playerState == BUFFERING)) {
176             return;
177         }
178
179         updateMediaInfoStatus(media);
180     }
181
182     private void updateMediaInfoStatus(final @Nullable Media media) {
183         State duration = UnDefType.UNDEF;
184         String metadataType = Media.MetadataType.GENERIC.name();
185         if (media != null) {
186             metadataType = media.getMetadataType().name();
187
188             // duration can be null when a new song is about to play.
189             if (media.duration != null) {
190                 duration = new QuantityType<>(media.duration, SmartHomeUnits.SECOND);
191             }
192         }
193
194         callback.updateState(CHANNEL_DURATION, duration);
195         callback.updateState(CHANNEL_METADATA_TYPE, new StringType(metadataType));
196
197         updateMetadataStatus(media == null || media.metadata == null ? Collections.emptyMap() : media.metadata);
198     }
199
200     private void updateMetadataStatus(Map<String, Object> metadata) {
201         updateLocation(metadata);
202         updateImage(metadata);
203
204         thing.getChannels().stream() //
205                 .map(channel -> channel.getUID())
206                 .filter(channelUID -> METADATA_SIMPLE_CHANNELS.contains(channelUID.getId()))
207                 .forEach(channelUID -> updateChannel(channelUID, metadata));
208     }
209
210     /** Lat/lon are combined into 1 channel so we have to handle them as a special case. */
211     private void updateLocation(Map<String, Object> metadata) {
212         if (!callback.isLinked(CHANNEL_LOCATION)) {
213             return;
214         }
215
216         Double lat = (Double) metadata.get(LOCATION_METADATA_LATITUDE);
217         Double lon = (Double) metadata.get(LOCATION_METADATA_LONGITUDE);
218         if (lat == null || lon == null) {
219             callback.updateState(CHANNEL_LOCATION, UnDefType.UNDEF);
220         } else {
221             PointType pointType = new PointType(new DecimalType(lat), new DecimalType(lon));
222             callback.updateState(CHANNEL_LOCATION, pointType);
223         }
224     }
225
226     private void updateImage(Map<String, Object> metadata) {
227         if (!(callback.isLinked(CHANNEL_IMAGE) || (callback.isLinked(CHANNEL_IMAGE_SRC)))) {
228             return;
229         }
230
231         // Channel name and metadata key don't match.
232         Object imagesValue = metadata.get("images");
233         if (imagesValue == null) {
234             callback.updateState(CHANNEL_IMAGE_SRC, UnDefType.UNDEF);
235             return;
236         }
237
238         String imageSrc = null;
239         @SuppressWarnings("unchecked")
240         List<Map<String, String>> strings = (List<Map<String, String>>) imagesValue;
241         for (Map<String, String> stringMap : strings) {
242             String url = stringMap.get("url");
243             if (url != null) {
244                 imageSrc = url;
245                 break;
246             }
247         }
248
249         if (callback.isLinked(CHANNEL_IMAGE_SRC)) {
250             callback.updateState(CHANNEL_IMAGE_SRC, imageSrc == null ? UnDefType.UNDEF : new StringType(imageSrc));
251         }
252
253         if (callback.isLinked(CHANNEL_IMAGE)) {
254             State image = imageSrc == null ? UnDefType.UNDEF : downloadImageFromCache(imageSrc);
255             callback.updateState(CHANNEL_IMAGE, image == null ? UnDefType.UNDEF : image);
256         }
257     }
258
259     private @Nullable RawType downloadImage(String url) {
260         logger.debug("Trying to download the content of URL '{}'", url);
261         RawType downloadedImage = HttpUtil.downloadImage(url);
262         if (downloadedImage == null) {
263             logger.debug("Failed to download the content of URL '{}'", url);
264         }
265         return downloadedImage;
266     }
267
268     private @Nullable RawType downloadImageFromCache(String url) {
269         if (IMAGE_CACHE.containsKey(url)) {
270             try {
271                 byte[] bytes = IMAGE_CACHE.get(url);
272                 String contentType = HttpUtil.guessContentTypeFromData(bytes);
273                 return new RawType(bytes,
274                         contentType == null || contentType.isEmpty() ? RawType.DEFAULT_MIME_TYPE : contentType);
275             } catch (IOException e) {
276                 logger.trace("Failed to download the content of URL '{}'", url, e);
277             }
278         } else {
279             RawType image = downloadImage(url);
280             if (image != null) {
281                 IMAGE_CACHE.put(url, image.getBytes());
282                 return image;
283             }
284         }
285         return null;
286     }
287
288     private void updateChannel(ChannelUID channelUID, Map<String, Object> metadata) {
289         if (!callback.isLinked(channelUID)) {
290             return;
291         }
292
293         Object value = getValue(channelUID.getId(), metadata);
294         State state;
295
296         if (value == null) {
297             state = UnDefType.UNDEF;
298         } else if (value instanceof Double) {
299             state = new DecimalType((Double) value);
300         } else if (value instanceof Integer) {
301             state = new DecimalType(((Integer) value).longValue());
302         } else if (value instanceof String) {
303             state = new StringType(value.toString());
304         } else if (value instanceof ZonedDateTime) {
305             state = new DateTimeType((ZonedDateTime) value);
306         } else {
307             state = UnDefType.UNDEF;
308             logger.warn("Update channel {}: Unsupported value type {}", channelUID, value.getClass().getSimpleName());
309         }
310
311         callback.updateState(channelUID, state);
312     }
313
314     private @Nullable Object getValue(String channelId, @Nullable Map<String, Object> metadata) {
315         if (metadata == null) {
316             return null;
317         }
318
319         if (CHANNEL_BROADCAST_DATE.equals(channelId) || CHANNEL_RELEASE_DATE.equals(channelId)
320                 || CHANNEL_CREATION_DATE.equals(channelId)) {
321             String dateString = (String) metadata.get(channelId);
322             return (dateString == null) ? null
323                     : ZonedDateTime.ofInstant(Instant.parse(dateString), ZoneId.systemDefault());
324         }
325
326         return metadata.get(channelId);
327     }
328
329     public void updateStatus(ThingStatus status) {
330         updateStatus(status, ThingStatusDetail.NONE, null);
331     }
332
333     public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
334         callback.updateStatus(status, statusDetail, description);
335     }
336 }