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