]> git.basschouten.com Git - openhab-addons.git/blob
fb52230170247a74d49cec0b0ef0b9770aca8ffa
[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.amazonechocontrol.internal.handler;
14
15 import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.URISyntaxException;
19 import java.time.Instant;
20 import java.time.ZoneId;
21 import java.time.ZonedDateTime;
22 import java.time.temporal.ChronoUnit;
23 import java.util.ArrayList;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Objects;
27 import java.util.Set;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30 import java.util.stream.Collectors;
31 import java.util.stream.Stream;
32
33 import org.apache.commons.lang.StringUtils;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.openhab.binding.amazonechocontrol.internal.Connection;
37 import org.openhab.binding.amazonechocontrol.internal.ConnectionException;
38 import org.openhab.binding.amazonechocontrol.internal.HttpException;
39 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandler;
40 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandlerAnnouncement;
41 import org.openhab.binding.amazonechocontrol.internal.channelhandler.IEchoThingHandler;
42 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity;
43 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity.Description;
44 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm.AscendingAlarmModel;
45 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates;
46 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState;
47 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.PairedDevice;
48 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushNotificationChange;
49 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushVolumeChange;
50 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState.DeviceNotificationState;
51 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device;
52 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonEqualizer;
53 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState;
54 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState.QueueEntry;
55 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider;
56 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse;
57 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound;
58 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState;
59 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo;
60 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.InfoText;
61 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.MainArt;
62 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Progress;
63 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Provider;
64 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Volume;
65 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists;
66 import org.openhab.core.library.types.DateTimeType;
67 import org.openhab.core.library.types.DecimalType;
68 import org.openhab.core.library.types.IncreaseDecreaseType;
69 import org.openhab.core.library.types.NextPreviousType;
70 import org.openhab.core.library.types.OnOffType;
71 import org.openhab.core.library.types.PercentType;
72 import org.openhab.core.library.types.PlayPauseType;
73 import org.openhab.core.library.types.QuantityType;
74 import org.openhab.core.library.types.RewindFastforwardType;
75 import org.openhab.core.library.types.StringType;
76 import org.openhab.core.library.unit.SmartHomeUnits;
77 import org.openhab.core.thing.Bridge;
78 import org.openhab.core.thing.ChannelUID;
79 import org.openhab.core.thing.Thing;
80 import org.openhab.core.thing.ThingStatus;
81 import org.openhab.core.thing.ThingUID;
82 import org.openhab.core.thing.binding.BaseThingHandler;
83 import org.openhab.core.types.Command;
84 import org.openhab.core.types.RefreshType;
85 import org.openhab.core.types.State;
86 import org.openhab.core.types.UnDefType;
87 import org.slf4j.Logger;
88 import org.slf4j.LoggerFactory;
89
90 import com.google.gson.Gson;
91
92 /**
93  * The {@link EchoHandler} is responsible for the handling of the echo device
94  *
95  * @author Michael Geramb - Initial contribution
96  */
97 @NonNullByDefault
98 public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
99
100     private final Logger logger = LoggerFactory.getLogger(EchoHandler.class);
101     private Gson gson;
102     private @Nullable Device device;
103     private Set<String> capabilities = new HashSet<>();
104     private @Nullable AccountHandler account;
105     private @Nullable ScheduledFuture<?> updateStateJob;
106     private @Nullable ScheduledFuture<?> updateProgressJob;
107     private Object progressLock = new Object();
108     private @Nullable String wakeWord;
109     private @Nullable String lastKnownRadioStationId;
110     private @Nullable String lastKnownBluetoothMAC;
111     private @Nullable String lastKnownAmazonMusicId;
112     private String musicProviderId = "TUNEIN";
113     private boolean isPlaying = false;
114     private boolean isPaused = false;
115     private int lastKnownVolume = 25;
116     private int textToSpeechVolume = 0;
117     private @Nullable JsonEqualizer lastKnownEqualizer = null;
118     private @Nullable BluetoothState bluetoothState;
119     private boolean disableUpdate = false;
120     private boolean updateRemind = true;
121     private boolean updateTextToSpeech = true;
122     private boolean updateAlarm = true;
123     private boolean updateRoutine = true;
124     private boolean updatePlayMusicVoiceCommand = true;
125     private boolean updateStartCommand = true;
126     private @Nullable Integer notificationVolumeLevel;
127     private @Nullable Boolean ascendingAlarm;
128     private @Nullable JsonPlaylists playLists;
129     private @Nullable JsonNotificationSound @Nullable [] alarmSounds;
130     private @Nullable List<JsonMusicProvider> musicProviders;
131     private List<ChannelHandler> channelHandlers = new ArrayList<>();
132
133     private @Nullable JsonNotificationResponse currentNotification;
134     private @Nullable ScheduledFuture<?> currentNotifcationUpdateTimer;
135     long mediaLengthMs;
136     long mediaProgressMs;
137     long mediaStartMs;
138     String lastSpokenText = "";
139
140     public EchoHandler(Thing thing, Gson gson) {
141         super(thing);
142         this.gson = gson;
143         channelHandlers.add(new ChannelHandlerAnnouncement(this, this.gson));
144     }
145
146     @Override
147     public void initialize() {
148         logger.debug("Amazon Echo Control Binding initialized");
149         Bridge bridge = this.getBridge();
150         if (bridge != null) {
151             AccountHandler account = (AccountHandler) bridge.getHandler();
152             if (account != null) {
153                 setDeviceAndUpdateThingState(account, this.device, null);
154                 account.addEchoHandler(this);
155             }
156         }
157     }
158
159     public boolean setDeviceAndUpdateThingState(AccountHandler accountHandler, @Nullable Device device,
160             @Nullable String wakeWord) {
161         this.account = accountHandler;
162         if (wakeWord != null) {
163             this.wakeWord = wakeWord;
164         }
165         if (device == null) {
166             updateStatus(ThingStatus.UNKNOWN);
167             return false;
168         }
169         this.device = device;
170         String[] capabilities = device.capabilities;
171         if (capabilities != null) {
172             this.capabilities = Stream.of(capabilities).filter(Objects::nonNull).collect(Collectors.toSet());
173         }
174         if (!device.online) {
175             updateStatus(ThingStatus.OFFLINE);
176             return false;
177         }
178         updateStatus(ThingStatus.ONLINE);
179         return true;
180     }
181
182     @Override
183     public void dispose() {
184         stopCurrentNotification();
185         ScheduledFuture<?> updateStateJob = this.updateStateJob;
186         this.updateStateJob = null;
187         if (updateStateJob != null) {
188             this.disableUpdate = false;
189             updateStateJob.cancel(false);
190         }
191         stopProgressTimer();
192         super.dispose();
193     }
194
195     private void stopProgressTimer() {
196         ScheduledFuture<?> updateProgressJob = this.updateProgressJob;
197         this.updateProgressJob = null;
198         if (updateProgressJob != null) {
199             updateProgressJob.cancel(false);
200         }
201     }
202
203     public @Nullable BluetoothState findBluetoothState() {
204         return this.bluetoothState;
205     }
206
207     public @Nullable JsonPlaylists findPlaylists() {
208         return this.playLists;
209     }
210
211     public @Nullable JsonNotificationSound @Nullable [] findAlarmSounds() {
212         return this.alarmSounds;
213     }
214
215     public @Nullable List<JsonMusicProvider> findMusicProviders() {
216         return this.musicProviders;
217     }
218
219     private @Nullable Connection findConnection() {
220         AccountHandler accountHandler = this.account;
221         if (accountHandler != null) {
222             return accountHandler.findConnection();
223         }
224         return null;
225     }
226
227     public @Nullable AccountHandler findAccount() {
228         return this.account;
229     }
230
231     public @Nullable Device findDevice() {
232         return this.device;
233     }
234
235     public String findSerialNumber() {
236         String id = (String) getConfig().get(DEVICE_PROPERTY_SERIAL_NUMBER);
237         if (id == null) {
238             return "";
239         }
240         return id;
241     }
242
243     @Override
244     public void handleCommand(ChannelUID channelUID, Command command) {
245         try {
246             logger.trace("Command '{}' received for channel '{}'", command, channelUID);
247             int waitForUpdate = 1000;
248             boolean needBluetoothRefresh = false;
249             String lastKnownBluetoothMAC = this.lastKnownBluetoothMAC;
250
251             ScheduledFuture<?> updateStateJob = this.updateStateJob;
252             this.updateStateJob = null;
253             if (updateStateJob != null) {
254                 this.disableUpdate = false;
255                 updateStateJob.cancel(false);
256             }
257             AccountHandler account = this.account;
258             if (account == null) {
259                 return;
260             }
261             Connection connection = account.findConnection();
262             if (connection == null) {
263                 return;
264             }
265             Device device = this.device;
266             if (device == null) {
267                 return;
268             }
269
270             String channelId = channelUID.getId();
271             for (ChannelHandler channelHandler : channelHandlers) {
272                 if (channelHandler.tryHandleCommand(device, connection, channelId, command)) {
273                     return;
274                 }
275             }
276
277             // Player commands
278             if (channelId.equals(CHANNEL_PLAYER)) {
279                 if (command == PlayPauseType.PAUSE || command == OnOffType.OFF) {
280                     connection.command(device, "{\"type\":\"PauseCommand\"}");
281                 } else if (command == PlayPauseType.PLAY || command == OnOffType.ON) {
282                     if (isPaused) {
283                         connection.command(device, "{\"type\":\"PlayCommand\"}");
284                     } else {
285                         connection.playMusicVoiceCommand(device, this.musicProviderId, "!");
286                         waitForUpdate = 3000;
287                     }
288                 } else if (command == NextPreviousType.NEXT) {
289                     connection.command(device, "{\"type\":\"NextCommand\"}");
290                 } else if (command == NextPreviousType.PREVIOUS) {
291                     connection.command(device, "{\"type\":\"PreviousCommand\"}");
292                 } else if (command == RewindFastforwardType.FASTFORWARD) {
293                     connection.command(device, "{\"type\":\"ForwardCommand\"}");
294                 } else if (command == RewindFastforwardType.REWIND) {
295                     connection.command(device, "{\"type\":\"RewindCommand\"}");
296                 }
297             }
298             // Notification commands
299             if (channelId.equals(CHANNEL_NOTIFICATION_VOLUME)) {
300                 if (command instanceof PercentType) {
301                     int volume = ((PercentType) command).intValue();
302                     connection.notificationVolume(device, volume);
303                     this.notificationVolumeLevel = volume;
304                     waitForUpdate = -1;
305                     account.forceCheckData();
306                 }
307             }
308             if (channelId.equals(CHANNEL_ASCENDING_ALARM)) {
309                 if (command == OnOffType.OFF) {
310                     connection.ascendingAlarm(device, false);
311                     this.ascendingAlarm = false;
312                     waitForUpdate = -1;
313                     account.forceCheckData();
314                 }
315                 if (command == OnOffType.ON) {
316                     connection.ascendingAlarm(device, true);
317                     this.ascendingAlarm = true;
318                     waitForUpdate = -1;
319                     account.forceCheckData();
320                 }
321             }
322             // Media progress commands
323             Long mediaPosition = null;
324             if (channelId.equals(CHANNEL_MEDIA_PROGRESS)) {
325                 if (command instanceof PercentType) {
326                     PercentType value = (PercentType) command;
327                     int percent = value.intValue();
328                     mediaPosition = Math.round((mediaLengthMs / 1000d) * (percent / 100d));
329                 }
330             }
331             if (channelId.equals(CHANNEL_MEDIA_PROGRESS_TIME)) {
332                 if (command instanceof DecimalType) {
333                     DecimalType value = (DecimalType) command;
334                     mediaPosition = value.longValue();
335                 }
336                 if (command instanceof QuantityType<?>) {
337                     QuantityType<?> value = (QuantityType<?>) command;
338                     @Nullable
339                     QuantityType<?> seconds = value.toUnit(SmartHomeUnits.SECOND);
340                     if (seconds != null) {
341                         mediaPosition = seconds.longValue();
342                     }
343                 }
344             }
345             if (mediaPosition != null) {
346                 waitForUpdate = -1;
347                 synchronized (progressLock) {
348                     String seekCommand = "{\"type\":\"SeekCommand\",\"mediaPosition\":" + mediaPosition
349                             + ",\"contentFocusClientId\":null}";
350                     connection.command(device, seekCommand);
351                     connection.command(device, seekCommand); // Must be sent twice, the first one is ignored sometimes
352                     this.mediaProgressMs = mediaPosition * 1000;
353                     mediaStartMs = System.currentTimeMillis() - this.mediaProgressMs;
354                     updateMediaProgress(false);
355                 }
356             }
357             // Volume commands
358             if (channelId.equals(CHANNEL_VOLUME)) {
359                 Integer volume = null;
360                 if (command instanceof PercentType) {
361                     PercentType value = (PercentType) command;
362                     volume = value.intValue();
363                 } else if (command == OnOffType.OFF) {
364                     volume = 0;
365                 } else if (command == OnOffType.ON) {
366                     volume = lastKnownVolume;
367                 } else if (command == IncreaseDecreaseType.INCREASE) {
368                     if (lastKnownVolume < 100) {
369                         lastKnownVolume++;
370                         volume = lastKnownVolume;
371                     }
372                 } else if (command == IncreaseDecreaseType.DECREASE) {
373                     if (lastKnownVolume > 0) {
374                         lastKnownVolume--;
375                         volume = lastKnownVolume;
376                     }
377                 }
378                 if (volume != null) {
379                     if (StringUtils.equals(device.deviceFamily, "WHA")) {
380                         connection.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + volume
381                                 + ",\"contentFocusClientId\":\"Default\"}");
382                     } else {
383                         connection.volume(device, volume);
384                     }
385                     lastKnownVolume = volume;
386                     updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume));
387                     waitForUpdate = -1;
388                 }
389             }
390             // equalizer commands
391             if (channelId.equals(CHANNEL_EQUALIZER_BASS) || channelId.equals(CHANNEL_EQUALIZER_MIDRANGE)
392                     || channelId.equals(CHANNEL_EQUALIZER_TREBLE)) {
393                 if (handleEqualizerCommands(channelId, command, connection, device)) {
394                     waitForUpdate = -1;
395                 }
396             }
397
398             // shuffle command
399             if (channelId.equals(CHANNEL_SHUFFLE)) {
400                 if (command instanceof OnOffType) {
401                     OnOffType value = (OnOffType) command;
402
403                     connection.command(device, "{\"type\":\"ShuffleCommand\",\"shuffle\":\""
404                             + (value == OnOffType.ON ? "true" : "false") + "\"}");
405                 }
406             }
407
408             // play music command
409             if (channelId.equals(CHANNEL_MUSIC_PROVIDER_ID)) {
410                 if (command instanceof StringType) {
411                     waitForUpdate = 0;
412                     String musicProviderId = ((StringType) command).toFullString();
413                     if (!StringUtils.equals(musicProviderId, this.musicProviderId)) {
414                         this.musicProviderId = musicProviderId;
415                         if (this.isPlaying) {
416                             connection.playMusicVoiceCommand(device, this.musicProviderId, "!");
417                             waitForUpdate = 3000;
418                         }
419                     }
420                 }
421             }
422             if (channelId.equals(CHANNEL_PLAY_MUSIC_VOICE_COMMAND)) {
423                 if (command instanceof StringType) {
424                     String voiceCommand = ((StringType) command).toFullString();
425                     if (!this.musicProviderId.isEmpty()) {
426                         connection.playMusicVoiceCommand(device, this.musicProviderId, voiceCommand);
427                         waitForUpdate = 3000;
428                         updatePlayMusicVoiceCommand = true;
429                     }
430                 }
431             }
432
433             // bluetooth commands
434             if (channelId.equals(CHANNEL_BLUETOOTH_MAC)) {
435                 needBluetoothRefresh = true;
436                 if (command instanceof StringType) {
437                     String address = ((StringType) command).toFullString();
438                     if (!address.isEmpty()) {
439                         waitForUpdate = 4000;
440                     }
441                     connection.bluetooth(device, address);
442                 }
443             }
444             if (channelId.equals(CHANNEL_BLUETOOTH)) {
445                 needBluetoothRefresh = true;
446                 if (command == OnOffType.ON) {
447                     waitForUpdate = 4000;
448                     String bluetoothId = lastKnownBluetoothMAC;
449                     BluetoothState state = bluetoothState;
450                     if (state != null && (StringUtils.isEmpty(bluetoothId))) {
451                         PairedDevice[] pairedDeviceList = state.pairedDeviceList;
452                         if (pairedDeviceList != null) {
453                             for (PairedDevice paired : pairedDeviceList) {
454                                 if (paired == null) {
455                                     continue;
456                                 }
457                                 if (StringUtils.isNotEmpty(paired.address)) {
458                                     lastKnownBluetoothMAC = paired.address;
459                                     break;
460                                 }
461                             }
462                         }
463                     }
464                     if (StringUtils.isNotEmpty(lastKnownBluetoothMAC)) {
465                         connection.bluetooth(device, lastKnownBluetoothMAC);
466                     }
467                 } else if (command == OnOffType.OFF) {
468                     connection.bluetooth(device, null);
469                 }
470             }
471             if (channelId.equals(CHANNEL_BLUETOOTH_DEVICE_NAME)) {
472                 needBluetoothRefresh = true;
473             }
474             // amazon music commands
475             if (channelId.equals(CHANNEL_AMAZON_MUSIC_TRACK_ID)) {
476                 if (command instanceof StringType) {
477                     String trackId = ((StringType) command).toFullString();
478                     if (StringUtils.isNotEmpty(trackId)) {
479                         waitForUpdate = 3000;
480                     }
481                     connection.playAmazonMusicTrack(device, trackId);
482                 }
483             }
484             if (channelId.equals(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID)) {
485                 if (command instanceof StringType) {
486                     String playListId = ((StringType) command).toFullString();
487                     if (StringUtils.isNotEmpty(playListId)) {
488                         waitForUpdate = 3000;
489                     }
490                     connection.playAmazonMusicPlayList(device, playListId);
491                 }
492             }
493             if (channelId.equals(CHANNEL_AMAZON_MUSIC)) {
494                 if (command == OnOffType.ON) {
495                     String lastKnownAmazonMusicId = this.lastKnownAmazonMusicId;
496                     if (StringUtils.isNotEmpty(lastKnownAmazonMusicId)) {
497                         waitForUpdate = 3000;
498                     }
499                     connection.playAmazonMusicTrack(device, lastKnownAmazonMusicId);
500                 } else if (command == OnOffType.OFF) {
501                     connection.playAmazonMusicTrack(device, "");
502                 }
503             }
504
505             // radio commands
506             if (channelId.equals(CHANNEL_RADIO_STATION_ID)) {
507                 if (command instanceof StringType) {
508                     String stationId = ((StringType) command).toFullString();
509                     if (StringUtils.isNotEmpty(stationId)) {
510                         waitForUpdate = 3000;
511                     }
512                     connection.playRadio(device, stationId);
513                 }
514             }
515             if (channelId.equals(CHANNEL_RADIO)) {
516                 if (command == OnOffType.ON) {
517                     String lastKnownRadioStationId = this.lastKnownRadioStationId;
518                     if (StringUtils.isNotEmpty(lastKnownRadioStationId)) {
519                         waitForUpdate = 3000;
520                     }
521                     connection.playRadio(device, lastKnownRadioStationId);
522                 } else if (command == OnOffType.OFF) {
523                     connection.playRadio(device, "");
524                 }
525             }
526
527             // notification
528             if (channelId.equals(CHANNEL_REMIND)) {
529                 if (command instanceof StringType) {
530                     stopCurrentNotification();
531                     String reminder = ((StringType) command).toFullString();
532                     if (StringUtils.isNotEmpty(reminder)) {
533                         waitForUpdate = 3000;
534                         updateRemind = true;
535                         currentNotification = connection.notification(device, "Reminder", reminder, null);
536                         currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> {
537                             updateNotificationTimerState();
538                         }, 1, 1, TimeUnit.SECONDS);
539                     }
540                 }
541             }
542             if (channelId.equals(CHANNEL_PLAY_ALARM_SOUND)) {
543                 if (command instanceof StringType) {
544                     stopCurrentNotification();
545                     String alarmSound = ((StringType) command).toFullString();
546                     if (StringUtils.isNotEmpty(alarmSound)) {
547                         waitForUpdate = 3000;
548                         updateAlarm = true;
549                         String[] parts = alarmSound.split(":", 2);
550                         JsonNotificationSound sound = new JsonNotificationSound();
551                         if (parts.length == 2) {
552                             sound.providerId = parts[0];
553                             sound.id = parts[1];
554                         } else {
555                             sound.providerId = "ECHO";
556                             sound.id = alarmSound;
557                         }
558                         currentNotification = connection.notification(device, "Alarm", null, sound);
559                         currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> {
560                             updateNotificationTimerState();
561                         }, 1, 1, TimeUnit.SECONDS);
562                     }
563                 }
564             }
565
566             // routine commands
567             if (channelId.equals(CHANNEL_TEXT_TO_SPEECH)) {
568                 if (command instanceof StringType) {
569                     String text = ((StringType) command).toFullString();
570                     if (StringUtils.isNotEmpty(text)) {
571                         waitForUpdate = 1000;
572                         updateTextToSpeech = true;
573                         startTextToSpeech(connection, device, text);
574                     }
575                 }
576             }
577             if (channelId.equals(CHANNEL_TEXT_TO_SPEECH_VOLUME)) {
578                 if (command instanceof PercentType) {
579                     PercentType value = (PercentType) command;
580                     textToSpeechVolume = value.intValue();
581                 } else if (command == OnOffType.OFF) {
582                     textToSpeechVolume = 0;
583                 } else if (command == OnOffType.ON) {
584                     textToSpeechVolume = lastKnownVolume;
585                 } else if (command == IncreaseDecreaseType.INCREASE) {
586                     if (textToSpeechVolume < 100) {
587                         textToSpeechVolume++;
588                     }
589                 } else if (command == IncreaseDecreaseType.DECREASE) {
590                     if (textToSpeechVolume > 0) {
591                         textToSpeechVolume--;
592                     }
593                 }
594                 this.updateState(channelId, new PercentType(textToSpeechVolume));
595             }
596             if (channelId.equals(CHANNEL_LAST_VOICE_COMMAND)) {
597                 if (command instanceof StringType) {
598                     String text = ((StringType) command).toFullString();
599                     if (StringUtils.isNotEmpty(text)) {
600                         waitForUpdate = -1;
601                         startTextToSpeech(connection, device, text);
602                     }
603                 }
604             }
605             if (channelId.equals(CHANNEL_START_COMMAND)) {
606                 if (command instanceof StringType) {
607                     String commandText = ((StringType) command).toFullString();
608                     if (StringUtils.isNotEmpty(commandText)) {
609                         updateStartCommand = true;
610                         if (commandText.startsWith(FLASH_BRIEFING_COMMAND_PREFIX)) {
611                             // Handle custom flashbriefings commands
612                             String flashbriefing = commandText.substring(FLASH_BRIEFING_COMMAND_PREFIX.length());
613
614                             for (FlashBriefingProfileHandler flashBriefing : account
615                                     .getFlashBriefingProfileHandlers()) {
616                                 ThingUID flashBriefingId = flashBriefing.getThing().getUID();
617                                 if (StringUtils.equals(flashBriefing.getThing().getUID().getId(), flashbriefing)) {
618                                     flashBriefing.handleCommand(new ChannelUID(flashBriefingId, CHANNEL_PLAY_ON_DEVICE),
619                                             new StringType(device.serialNumber));
620                                     break;
621                                 }
622                             }
623                         } else {
624                             // Handle standard commands
625                             if (!commandText.startsWith("Alexa.")) {
626                                 commandText = "Alexa." + commandText + ".Play";
627                             }
628                             waitForUpdate = 1000;
629                             connection.executeSequenceCommand(device, commandText, null);
630                         }
631                     }
632                 }
633             }
634             if (channelId.equals(CHANNEL_START_ROUTINE)) {
635                 if (command instanceof StringType) {
636                     String utterance = ((StringType) command).toFullString();
637                     if (StringUtils.isNotEmpty(utterance)) {
638                         waitForUpdate = 1000;
639                         updateRoutine = true;
640                         connection.startRoutine(device, utterance);
641                     }
642                 }
643             }
644             if (waitForUpdate < 0) {
645                 return;
646             }
647             // force update of the state
648             this.disableUpdate = true;
649             final boolean bluetoothRefresh = needBluetoothRefresh;
650             Runnable doRefresh = () -> {
651                 this.disableUpdate = false;
652                 BluetoothState state = null;
653                 if (bluetoothRefresh) {
654                     JsonBluetoothStates states;
655                     states = connection.getBluetoothConnectionStates();
656                     if (states != null) {
657                         state = states.findStateByDevice(device);
658                     }
659                 }
660
661                 updateState(account, device, state, null, null, null, null, null);
662             };
663             if (command instanceof RefreshType) {
664                 waitForUpdate = 0;
665                 account.forceCheckData();
666             }
667             if (waitForUpdate == 0) {
668                 doRefresh.run();
669             } else {
670                 this.updateStateJob = scheduler.schedule(doRefresh, waitForUpdate, TimeUnit.MILLISECONDS);
671             }
672         } catch (IOException | URISyntaxException e) {
673             logger.info("handleCommand fails", e);
674         }
675     }
676
677     private boolean handleEqualizerCommands(String channelId, Command command, Connection connection, Device device)
678             throws URISyntaxException {
679         if (command instanceof RefreshType) {
680             this.lastKnownEqualizer = null;
681         }
682         if (command instanceof DecimalType) {
683             DecimalType value = (DecimalType) command;
684             if (this.lastKnownEqualizer == null) {
685                 updateEqualizerState();
686             }
687             JsonEqualizer lastKnownEqualizer = this.lastKnownEqualizer;
688             if (lastKnownEqualizer != null) {
689                 JsonEqualizer newEqualizerSetting = lastKnownEqualizer.createClone();
690                 if (channelId.equals(CHANNEL_EQUALIZER_BASS)) {
691                     newEqualizerSetting.bass = value.intValue();
692                 }
693                 if (channelId.equals(CHANNEL_EQUALIZER_MIDRANGE)) {
694                     newEqualizerSetting.mid = value.intValue();
695                 }
696                 if (channelId.equals(CHANNEL_EQUALIZER_TREBLE)) {
697                     newEqualizerSetting.treble = value.intValue();
698                 }
699                 try {
700                     connection.setEqualizer(device, newEqualizerSetting);
701                     return true;
702                 } catch (HttpException | IOException | ConnectionException e) {
703                     logger.debug("Update equalizer failed", e);
704                     this.lastKnownEqualizer = null;
705                 }
706             }
707         }
708         return false;
709     }
710
711     private void startTextToSpeech(Connection connection, Device device, String text)
712             throws IOException, URISyntaxException {
713         Integer volume = null;
714         if (textToSpeechVolume != 0) {
715             volume = textToSpeechVolume;
716         }
717         connection.textToSpeech(device, text, volume, lastKnownVolume);
718     }
719
720     @Override
721     public void startAnnouncment(Device device, String speak, String bodyText, @Nullable String title,
722             @Nullable Integer volume) throws IOException, URISyntaxException {
723         Connection connection = this.findConnection();
724         if (connection == null) {
725             return;
726         }
727         if (volume == null && textToSpeechVolume != 0) {
728             volume = textToSpeechVolume;
729         }
730         if (volume != null && volume < 0) {
731             volume = null; // the meaning of negative values is 'do not use'. The api requires null in this case.
732         }
733         connection.announcement(device, speak, bodyText, title, volume, lastKnownVolume);
734     }
735
736     private void stopCurrentNotification() {
737         ScheduledFuture<?> currentNotifcationUpdateTimer = this.currentNotifcationUpdateTimer;
738         if (currentNotifcationUpdateTimer != null) {
739             this.currentNotifcationUpdateTimer = null;
740             currentNotifcationUpdateTimer.cancel(true);
741         }
742         JsonNotificationResponse currentNotification = this.currentNotification;
743         if (currentNotification != null) {
744             this.currentNotification = null;
745             Connection currentConnection = this.findConnection();
746             if (currentConnection != null) {
747                 try {
748                     currentConnection.stopNotification(currentNotification);
749                 } catch (IOException | URISyntaxException e) {
750                     logger.warn("Stop notification failed", e);
751                 }
752             }
753         }
754     }
755
756     private void updateNotificationTimerState() {
757         boolean stopCurrentNotification = true;
758         JsonNotificationResponse currentNotification = this.currentNotification;
759         try {
760             if (currentNotification != null) {
761                 Connection currentConnection = this.findConnection();
762                 if (currentConnection != null) {
763                     JsonNotificationResponse newState = currentConnection.getNotificationState(currentNotification);
764                     if (newState != null && "ON".equals(newState.status)) {
765                         stopCurrentNotification = false;
766                     }
767                 }
768             }
769         } catch (IOException | URISyntaxException e) {
770             logger.warn("update notification state fails", e);
771         }
772         if (stopCurrentNotification) {
773             if (currentNotification != null) {
774                 String type = currentNotification.type;
775                 if (type != null) {
776                     if (type.equals("Reminder")) {
777                         updateState(CHANNEL_REMIND, new StringType(""));
778                         updateRemind = false;
779                     }
780                     if (type.equals("Alarm")) {
781                         updateState(CHANNEL_PLAY_ALARM_SOUND, new StringType(""));
782                         updateAlarm = false;
783                     }
784                 }
785             }
786             stopCurrentNotification();
787         }
788     }
789
790     public void updateState(AccountHandler accountHandler, @Nullable Device device,
791             @Nullable BluetoothState bluetoothState, @Nullable DeviceNotificationState deviceNotificationState,
792             @Nullable AscendingAlarmModel ascendingAlarmModel, @Nullable JsonPlaylists playlists,
793             @Nullable JsonNotificationSound @Nullable [] alarmSounds,
794             @Nullable List<JsonMusicProvider> musicProviders) {
795         try {
796             this.logger.debug("Handle updateState {}", this.getThing().getUID());
797
798             if (deviceNotificationState != null) {
799                 notificationVolumeLevel = deviceNotificationState.volumeLevel;
800             }
801             if (ascendingAlarmModel != null) {
802                 ascendingAlarm = ascendingAlarmModel.ascendingAlarmEnabled;
803             }
804             if (playlists != null) {
805                 this.playLists = playlists;
806             }
807             if (alarmSounds != null) {
808                 this.alarmSounds = alarmSounds;
809             }
810             if (musicProviders != null) {
811                 this.musicProviders = musicProviders;
812             }
813             if (!setDeviceAndUpdateThingState(accountHandler, device, null)) {
814                 this.logger.debug("Handle updateState {} aborted: Not online", this.getThing().getUID());
815                 return;
816             }
817             if (device == null) {
818                 this.logger.debug("Handle updateState {} aborted: No device", this.getThing().getUID());
819                 return;
820             }
821
822             if (this.disableUpdate) {
823                 this.logger.debug("Handle updateState {} aborted: Disabled", this.getThing().getUID());
824                 return;
825             }
826             Connection connection = this.findConnection();
827             if (connection == null) {
828                 return;
829             }
830
831             if (this.lastKnownEqualizer == null) {
832                 updateEqualizerState();
833             }
834
835             PlayerInfo playerInfo = null;
836             Provider provider = null;
837             InfoText infoText = null;
838             MainArt mainArt = null;
839             String musicProviderId = null;
840             Progress progress = null;
841             try {
842                 JsonPlayerState playerState = connection.getPlayer(device);
843                 if (playerState != null) {
844                     playerInfo = playerState.playerInfo;
845                     if (playerInfo != null) {
846                         infoText = playerInfo.infoText;
847                         if (infoText == null) {
848                             infoText = playerInfo.miniInfoText;
849                         }
850                         mainArt = playerInfo.mainArt;
851                         provider = playerInfo.provider;
852                         if (provider != null) {
853                             musicProviderId = provider.providerName;
854                             // Map the music provider id to the one used for starting music with voice command
855                             if (musicProviderId != null) {
856                                 musicProviderId = musicProviderId.toUpperCase();
857
858                                 if (StringUtils.equals(musicProviderId, "AMAZON MUSIC")) {
859                                     musicProviderId = "AMAZON_MUSIC";
860                                 }
861                                 if (StringUtils.equals(musicProviderId, "CLOUD_PLAYER")) {
862                                     musicProviderId = "AMAZON_MUSIC";
863                                 }
864                                 if (StringUtils.startsWith(musicProviderId, "TUNEIN")) {
865                                     musicProviderId = "TUNEIN";
866                                 }
867                                 if (StringUtils.startsWithIgnoreCase(musicProviderId, "iHeartRadio")) {
868                                     musicProviderId = "I_HEART_RADIO";
869                                 }
870                                 if (StringUtils.containsIgnoreCase(musicProviderId, "Apple")
871                                         && StringUtils.containsIgnoreCase(musicProviderId, "Music")) {
872                                     musicProviderId = "APPLE_MUSIC";
873                                 }
874                             }
875                         }
876                         progress = playerInfo.progress;
877                     }
878                 }
879             } catch (HttpException e) {
880                 if (e.getCode() != 400) {
881                     logger.info("getPlayer fails", e);
882                 }
883             } catch (IOException | URISyntaxException e) {
884                 logger.info("getPlayer fails", e);
885             }
886             // check playing
887             isPlaying = (playerInfo != null && StringUtils.equals(playerInfo.state, "PLAYING"));
888
889             isPaused = (playerInfo != null && StringUtils.equals(playerInfo.state, "PAUSED"));
890             synchronized (progressLock) {
891                 Boolean showTime = null;
892                 Long mediaLength = null;
893                 Long mediaProgress = null;
894                 if (progress != null) {
895                     showTime = progress.showTiming;
896                     mediaLength = progress.mediaLength;
897                     mediaProgress = progress.mediaProgress;
898                 }
899                 if (showTime != null && showTime && mediaProgress != null && mediaLength != null) {
900                     mediaProgressMs = mediaProgress * 1000;
901                     mediaLengthMs = mediaLength * 1000;
902                     mediaStartMs = System.currentTimeMillis() - mediaProgressMs;
903                     if (isPlaying) {
904                         if (updateProgressJob == null) {
905                             updateProgressJob = scheduler.scheduleWithFixedDelay(this::updateMediaProgress, 1000, 1000,
906                                     TimeUnit.MILLISECONDS);
907                         }
908                     } else {
909                         stopProgressTimer();
910                     }
911                 } else {
912                     stopProgressTimer();
913                     mediaProgressMs = 0;
914                     mediaStartMs = 0;
915                     mediaLengthMs = 0;
916                 }
917                 updateMediaProgress(true);
918             }
919
920             JsonMediaState mediaState = null;
921             try {
922                 if (StringUtils.equalsIgnoreCase(musicProviderId, "AMAZON_MUSIC")
923                         || StringUtils.equalsIgnoreCase(musicProviderId, "TUNEIN")) {
924                     mediaState = connection.getMediaState(device);
925                 }
926             } catch (HttpException e) {
927                 if (e.getCode() == 400) {
928                     updateState(CHANNEL_RADIO_STATION_ID, new StringType(""));
929                 } else {
930                     logger.info("getMediaState fails", e);
931                 }
932             } catch (IOException | URISyntaxException e) {
933                 logger.info("getMediaState fails", e);
934             }
935
936             // handle music provider id
937             if (provider != null && isPlaying) {
938                 if (musicProviderId != null) {
939                     this.musicProviderId = musicProviderId;
940                 }
941             }
942
943             // handle amazon music
944             String amazonMusicTrackId = "";
945             String amazonMusicPlayListId = "";
946             boolean amazonMusic = false;
947             if (mediaState != null && isPlaying && StringUtils.equals(mediaState.providerId, "CLOUD_PLAYER")
948                     && StringUtils.isNotEmpty(mediaState.contentId)) {
949                 amazonMusicTrackId = mediaState.contentId;
950                 lastKnownAmazonMusicId = amazonMusicTrackId;
951                 amazonMusic = true;
952             }
953
954             // handle bluetooth
955             String bluetoothMAC = "";
956             String bluetoothDeviceName = "";
957             boolean bluetoothIsConnected = false;
958             if (bluetoothState != null) {
959                 this.bluetoothState = bluetoothState;
960                 PairedDevice[] pairedDeviceList = bluetoothState.pairedDeviceList;
961                 if (pairedDeviceList != null) {
962                     for (PairedDevice paired : pairedDeviceList) {
963                         if (paired == null) {
964                             continue;
965                         }
966                         if (paired.connected && paired.address != null) {
967                             bluetoothIsConnected = true;
968                             bluetoothMAC = paired.address;
969                             bluetoothDeviceName = paired.friendlyName;
970                             if (StringUtils.isEmpty(bluetoothDeviceName)) {
971                                 bluetoothDeviceName = paired.address;
972                             }
973                             break;
974                         }
975                     }
976                 }
977             }
978             if (StringUtils.isNotEmpty(bluetoothMAC)) {
979                 lastKnownBluetoothMAC = bluetoothMAC;
980             }
981
982             // handle radio
983             boolean isRadio = false;
984             if (mediaState != null && StringUtils.isNotEmpty(mediaState.radioStationId)) {
985                 lastKnownRadioStationId = mediaState.radioStationId;
986                 if (StringUtils.equalsIgnoreCase(musicProviderId, "TUNEIN")) {
987                     isRadio = true;
988                 }
989             }
990             String radioStationId = "";
991             if (isRadio && mediaState != null && StringUtils.equals(mediaState.currentState, "PLAYING")
992                     && mediaState.radioStationId != null) {
993                 radioStationId = mediaState.radioStationId;
994             }
995
996             // handle title, subtitle, imageUrl
997             String title = "";
998             String subTitle1 = "";
999             String subTitle2 = "";
1000             String imageUrl = "";
1001             if (infoText != null) {
1002                 if (infoText.title != null) {
1003                     title = infoText.title;
1004                 }
1005                 if (infoText.subText1 != null) {
1006                     subTitle1 = infoText.subText1;
1007                 }
1008
1009                 if (infoText.subText2 != null) {
1010                     subTitle2 = infoText.subText2;
1011                 }
1012             }
1013             if (mainArt != null) {
1014                 if (mainArt.url != null) {
1015                     imageUrl = mainArt.url;
1016                 }
1017             }
1018             if (mediaState != null) {
1019                 QueueEntry[] queueEntries = mediaState.queue;
1020                 if (queueEntries != null && queueEntries.length > 0) {
1021                     QueueEntry entry = queueEntries[0];
1022                     if (entry != null) {
1023                         if (isRadio) {
1024                             if (StringUtils.isEmpty(imageUrl) && entry.imageURL != null) {
1025                                 imageUrl = entry.imageURL;
1026                             }
1027                             if (StringUtils.isEmpty(subTitle1) && entry.radioStationSlogan != null) {
1028                                 subTitle1 = entry.radioStationSlogan;
1029                             }
1030                             if (StringUtils.isEmpty(subTitle2) && entry.radioStationLocation != null) {
1031                                 subTitle2 = entry.radioStationLocation;
1032                             }
1033                         }
1034                     }
1035                 }
1036             }
1037
1038             // handle provider
1039             String providerDisplayName = "";
1040             if (provider != null) {
1041                 if (provider.providerDisplayName != null) {
1042                     providerDisplayName = provider.providerDisplayName;
1043                 }
1044                 if (StringUtils.isNotEmpty(provider.providerName) && StringUtils.isEmpty(providerDisplayName)) {
1045                     providerDisplayName = provider.providerName;
1046                 }
1047             }
1048
1049             // handle volume
1050             Integer volume = null;
1051             if (!connection.isSequenceNodeQueueRunning()) {
1052                 if (mediaState != null) {
1053                     volume = mediaState.volume;
1054                 }
1055                 if (playerInfo != null && volume == null) {
1056                     Volume volumnInfo = playerInfo.volume;
1057                     if (volumnInfo != null) {
1058                         volume = volumnInfo.volume;
1059                     }
1060                 }
1061                 if (volume != null && volume > 0) {
1062                     lastKnownVolume = volume;
1063                 }
1064                 if (volume == null) {
1065                     volume = lastKnownVolume;
1066                 }
1067             }
1068             // Update states
1069             if (updateRemind && currentNotifcationUpdateTimer == null) {
1070                 updateRemind = false;
1071                 updateState(CHANNEL_REMIND, new StringType(""));
1072             }
1073             if (updateAlarm && currentNotifcationUpdateTimer == null) {
1074                 updateAlarm = false;
1075                 updateState(CHANNEL_PLAY_ALARM_SOUND, new StringType(""));
1076             }
1077             if (updateRoutine) {
1078                 updateRoutine = false;
1079                 updateState(CHANNEL_START_ROUTINE, new StringType(""));
1080             }
1081             if (updateTextToSpeech) {
1082                 updateTextToSpeech = false;
1083                 updateState(CHANNEL_TEXT_TO_SPEECH, new StringType(""));
1084             }
1085             if (updatePlayMusicVoiceCommand) {
1086                 updatePlayMusicVoiceCommand = false;
1087                 updateState(CHANNEL_PLAY_MUSIC_VOICE_COMMAND, new StringType(""));
1088             }
1089             if (updateStartCommand) {
1090                 updateStartCommand = false;
1091                 updateState(CHANNEL_START_COMMAND, new StringType(""));
1092             }
1093
1094             updateState(CHANNEL_MUSIC_PROVIDER_ID, new StringType(musicProviderId));
1095             updateState(CHANNEL_AMAZON_MUSIC_TRACK_ID, new StringType(amazonMusicTrackId));
1096             updateState(CHANNEL_AMAZON_MUSIC, isPlaying && amazonMusic ? OnOffType.ON : OnOffType.OFF);
1097             updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID, new StringType(amazonMusicPlayListId));
1098             updateState(CHANNEL_RADIO_STATION_ID, new StringType(radioStationId));
1099             updateState(CHANNEL_RADIO, isPlaying && isRadio ? OnOffType.ON : OnOffType.OFF);
1100             updateState(CHANNEL_PROVIDER_DISPLAY_NAME, new StringType(providerDisplayName));
1101             updateState(CHANNEL_PLAYER, isPlaying ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
1102             updateState(CHANNEL_IMAGE_URL, new StringType(imageUrl));
1103             updateState(CHANNEL_TITLE, new StringType(title));
1104             if (volume != null) {
1105                 updateState(CHANNEL_VOLUME, new PercentType(volume));
1106             }
1107             updateState(CHANNEL_SUBTITLE1, new StringType(subTitle1));
1108             updateState(CHANNEL_SUBTITLE2, new StringType(subTitle2));
1109             if (bluetoothState != null) {
1110                 updateState(CHANNEL_BLUETOOTH, bluetoothIsConnected ? OnOffType.ON : OnOffType.OFF);
1111                 updateState(CHANNEL_BLUETOOTH_MAC, new StringType(bluetoothMAC));
1112                 updateState(CHANNEL_BLUETOOTH_DEVICE_NAME, new StringType(bluetoothDeviceName));
1113             }
1114
1115             updateState(CHANNEL_ASCENDING_ALARM,
1116                     ascendingAlarm != null ? (ascendingAlarm ? OnOffType.ON : OnOffType.OFF) : UnDefType.UNDEF);
1117
1118             final Integer notificationVolumeLevel = this.notificationVolumeLevel;
1119             if (notificationVolumeLevel != null) {
1120                 updateState(CHANNEL_NOTIFICATION_VOLUME, new PercentType(notificationVolumeLevel));
1121             } else {
1122                 updateState(CHANNEL_NOTIFICATION_VOLUME, UnDefType.UNDEF);
1123             }
1124         } catch (Exception e) {
1125             this.logger.debug("Handle updateState {} failed: {}", this.getThing().getUID(), e.getMessage(), e);
1126
1127             disableUpdate = false;
1128             throw e; // Rethrow same exception
1129         }
1130     }
1131
1132     private void updateEqualizerState() {
1133         if (!this.capabilities.contains("SOUND_SETTINGS")) {
1134             return;
1135         }
1136
1137         Connection connection = findConnection();
1138         if (connection == null) {
1139             return;
1140         }
1141         Device device = findDevice();
1142         if (device == null) {
1143             return;
1144         }
1145         Integer bass = null;
1146         Integer midrange = null;
1147         Integer treble = null;
1148         try {
1149             JsonEqualizer equalizer = connection.getEqualizer(device);
1150             if (equalizer != null) {
1151                 bass = equalizer.bass;
1152                 midrange = equalizer.mid;
1153                 treble = equalizer.treble;
1154             }
1155             this.lastKnownEqualizer = equalizer;
1156         } catch (IOException | URISyntaxException | HttpException | ConnectionException e) {
1157             logger.debug("Get equalizer failes", e);
1158             return;
1159         }
1160         if (bass != null) {
1161             updateState(CHANNEL_EQUALIZER_BASS, new DecimalType(bass));
1162         }
1163         if (midrange != null) {
1164             updateState(CHANNEL_EQUALIZER_MIDRANGE, new DecimalType(midrange));
1165         }
1166         if (treble != null) {
1167             updateState(CHANNEL_EQUALIZER_TREBLE, new DecimalType(treble));
1168         }
1169     }
1170
1171     private void updateMediaProgress() {
1172         updateMediaProgress(false);
1173     }
1174
1175     private void updateMediaProgress(boolean updateMediaLength) {
1176         synchronized (progressLock) {
1177             if (mediaStartMs > 0) {
1178                 long currentPlayTimeMs = isPlaying ? System.currentTimeMillis() - mediaStartMs : mediaProgressMs;
1179                 if (mediaLengthMs > 0) {
1180                     int progressPercent = (int) Math.min(100,
1181                             Math.round((double) currentPlayTimeMs / (double) mediaLengthMs * 100));
1182                     updateState(CHANNEL_MEDIA_PROGRESS, new PercentType(progressPercent));
1183                 } else {
1184                     updateState(CHANNEL_MEDIA_PROGRESS, UnDefType.UNDEF);
1185                 }
1186                 updateState(CHANNEL_MEDIA_PROGRESS_TIME,
1187                         new QuantityType<>(currentPlayTimeMs / 1000, SmartHomeUnits.SECOND));
1188                 if (updateMediaLength) {
1189                     updateState(CHANNEL_MEDIA_LENGTH, new QuantityType<>(mediaLengthMs / 1000, SmartHomeUnits.SECOND));
1190                 }
1191             } else {
1192                 updateState(CHANNEL_MEDIA_PROGRESS, UnDefType.UNDEF);
1193                 updateState(CHANNEL_MEDIA_LENGTH, UnDefType.UNDEF);
1194                 updateState(CHANNEL_MEDIA_PROGRESS_TIME, UnDefType.UNDEF);
1195                 if (updateMediaLength) {
1196                     updateState(CHANNEL_MEDIA_LENGTH, UnDefType.UNDEF);
1197                 }
1198             }
1199         }
1200     }
1201
1202     public void handlePushActivity(Activity pushActivity) {
1203         if ("DISCARDED_NON_DEVICE_DIRECTED_INTENT".equals(pushActivity.activityStatus)) {
1204             return;
1205         }
1206         Description description = pushActivity.parseDescription();
1207         if (StringUtils.isEmpty(description.firstUtteranceId)
1208                 || StringUtils.startsWithIgnoreCase(description.firstUtteranceId, "TextClient:")) {
1209             return;
1210         }
1211         if (StringUtils.isEmpty(description.firstStreamId)) {
1212             return;
1213         }
1214         String spokenText = description.summary;
1215         if (spokenText != null && StringUtils.isNotEmpty(spokenText)) {
1216             // remove wake word
1217             String wakeWordPrefix = this.wakeWord;
1218             if (wakeWordPrefix != null) {
1219                 wakeWordPrefix += " ";
1220                 if (StringUtils.startsWithIgnoreCase(spokenText, wakeWordPrefix)) {
1221                     spokenText = spokenText.substring(wakeWordPrefix.length());
1222                 }
1223             }
1224
1225             if (lastSpokenText.isEmpty() || lastSpokenText.equals(spokenText)) {
1226                 updateState(CHANNEL_LAST_VOICE_COMMAND, new StringType(""));
1227             }
1228             lastSpokenText = spokenText;
1229             updateState(CHANNEL_LAST_VOICE_COMMAND, new StringType(spokenText));
1230         }
1231     }
1232
1233     public void handlePushCommand(String command, String payload) {
1234         this.logger.debug("Handle push command {}", command);
1235         switch (command) {
1236             case "PUSH_VOLUME_CHANGE":
1237                 JsonCommandPayloadPushVolumeChange volumeChange = gson.fromJson(payload,
1238                         JsonCommandPayloadPushVolumeChange.class);
1239                 Connection connection = this.findConnection();
1240                 @Nullable
1241                 Integer volumeSetting = volumeChange.volumeSetting;
1242                 @Nullable
1243                 Boolean muted = volumeChange.isMuted;
1244                 if (muted != null && muted) {
1245                     updateState(CHANNEL_VOLUME, new PercentType(0));
1246                 }
1247                 if (volumeSetting != null && connection != null && !connection.isSequenceNodeQueueRunning()) {
1248                     lastKnownVolume = volumeSetting;
1249                     updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume));
1250                 }
1251                 break;
1252             case "PUSH_EQUALIZER_STATE_CHANGE":
1253                 updateEqualizerState();
1254                 break;
1255             default:
1256                 AccountHandler account = this.account;
1257                 Device device = this.device;
1258                 if (account != null && device != null) {
1259                     this.disableUpdate = false;
1260                     updateState(account, device, null, null, null, null, null, null);
1261                 }
1262         }
1263     }
1264
1265     public void updateNotifications(ZonedDateTime currentTime, ZonedDateTime now,
1266             @Nullable JsonCommandPayloadPushNotificationChange pushPayload, JsonNotificationResponse[] notifications) {
1267         Device device = this.device;
1268         if (device == null) {
1269             return;
1270         }
1271
1272         ZonedDateTime nextReminder = null;
1273         ZonedDateTime nextAlarm = null;
1274         ZonedDateTime nextMusicAlarm = null;
1275         ZonedDateTime nextTimer = null;
1276         for (JsonNotificationResponse notification : notifications) {
1277             if (StringUtils.equals(notification.deviceSerialNumber, device.serialNumber)) {
1278                 // notification for this device
1279                 if (StringUtils.equals(notification.status, "ON")) {
1280                     if ("Reminder".equals(notification.type)) {
1281                         String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1282                         ZonedDateTime alarmTime = ZonedDateTime
1283                                 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1284                         if (StringUtils.isNotBlank(notification.recurringPattern) && alarmTime.isBefore(now)) {
1285                             continue; // Ignore recurring entry if alarm time is before now
1286                         }
1287                         if (nextReminder == null || alarmTime.isBefore(nextReminder)) {
1288                             nextReminder = alarmTime;
1289                         }
1290                     } else if ("Timer".equals(notification.type)) {
1291                         // use remaining time
1292                         ZonedDateTime alarmTime = currentTime.plus(notification.remainingTime, ChronoUnit.MILLIS);
1293                         if (nextTimer == null || alarmTime.isBefore(nextTimer)) {
1294                             nextTimer = alarmTime;
1295                         }
1296                     } else if ("Alarm".equals(notification.type)) {
1297                         String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1298                         ZonedDateTime alarmTime = ZonedDateTime
1299                                 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1300                         if (StringUtils.isNotBlank(notification.recurringPattern) && alarmTime.isBefore(now)) {
1301                             continue; // Ignore recurring entry if alarm time is before now
1302                         }
1303                         if (nextAlarm == null || alarmTime.isBefore(nextAlarm)) {
1304                             nextAlarm = alarmTime;
1305                         }
1306                     } else if ("MusicAlarm".equals(notification.type)) {
1307                         String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1308                         ZonedDateTime alarmTime = ZonedDateTime
1309                                 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1310                         if (StringUtils.isNotBlank(notification.recurringPattern) && alarmTime.isBefore(now)) {
1311                             continue; // Ignore recurring entry if alarm time is before now
1312                         }
1313                         if (nextMusicAlarm == null || alarmTime.isBefore(nextMusicAlarm)) {
1314                             nextMusicAlarm = alarmTime;
1315                         }
1316                     }
1317                 }
1318             }
1319         }
1320
1321         updateState(CHANNEL_NEXT_REMINDER, nextReminder == null ? UnDefType.UNDEF : new DateTimeType(nextReminder));
1322         updateState(CHANNEL_NEXT_ALARM, nextAlarm == null ? UnDefType.UNDEF : new DateTimeType(nextAlarm));
1323         updateState(CHANNEL_NEXT_MUSIC_ALARM,
1324                 nextMusicAlarm == null ? UnDefType.UNDEF : new DateTimeType(nextMusicAlarm));
1325         updateState(CHANNEL_NEXT_TIMER, nextTimer == null ? UnDefType.UNDEF : new DateTimeType(nextTimer));
1326     }
1327
1328     @Override
1329     public void updateChannelState(String channelId, State state) {
1330         updateState(channelId, state);
1331     }
1332 }