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