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