]> git.basschouten.com Git - openhab-addons.git/blob
2ec69eb17d8207b4032ec669d8227eaaf86597b6
[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 percentCommand) {
297                     int volume = percentCommand.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 percentCommand) {
322                     int percent = percentCommand.intValue();
323                     mediaPosition = Math.round((mediaLengthMs / 1000d) * (percent / 100d));
324                 }
325             }
326             if (channelId.equals(CHANNEL_MEDIA_PROGRESS_TIME)) {
327                 if (command instanceof DecimalType decimalCommand) {
328                     mediaPosition = decimalCommand.longValue();
329                 }
330                 if (command instanceof QuantityType<?> quantityCommand) {
331                     @Nullable
332                     QuantityType<?> seconds = quantityCommand.toUnit(Units.SECOND);
333                     if (seconds != null) {
334                         mediaPosition = seconds.longValue();
335                     }
336                 }
337             }
338             if (mediaPosition != null) {
339                 waitForUpdate = -1;
340                 synchronized (progressLock) {
341                     String seekCommand = "{\"type\":\"SeekCommand\",\"mediaPosition\":" + mediaPosition
342                             + ",\"contentFocusClientId\":null}";
343                     connection.command(device, seekCommand);
344                     connection.command(device, seekCommand); // Must be sent twice, the first one is ignored sometimes
345                     this.mediaProgressMs = mediaPosition * 1000;
346                     mediaStartMs = System.currentTimeMillis() - this.mediaProgressMs;
347                     updateMediaProgress(false);
348                 }
349             }
350             // Volume commands
351             if (channelId.equals(CHANNEL_VOLUME)) {
352                 Integer volume = null;
353                 if (command instanceof PercentType percentCommand) {
354                     volume = percentCommand.intValue();
355                 } else if (command == OnOffType.OFF) {
356                     volume = 0;
357                 } else if (command == OnOffType.ON) {
358                     volume = lastKnownVolume;
359                 } else if (command == IncreaseDecreaseType.INCREASE) {
360                     if (lastKnownVolume < 100) {
361                         lastKnownVolume++;
362                         volume = lastKnownVolume;
363                     }
364                 } else if (command == IncreaseDecreaseType.DECREASE) {
365                     if (lastKnownVolume > 0) {
366                         lastKnownVolume--;
367                         volume = lastKnownVolume;
368                     }
369                 }
370                 if (volume != null) {
371                     if ("WHA".equals(device.deviceFamily)) {
372                         connection.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + volume
373                                 + ",\"contentFocusClientId\":\"Default\"}");
374                     } else {
375                         connection.volume(device, volume);
376                     }
377                     lastKnownVolume = volume;
378                     updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume));
379                     waitForUpdate = -1;
380                 }
381             }
382             // equalizer commands
383             if (channelId.equals(CHANNEL_EQUALIZER_BASS) || channelId.equals(CHANNEL_EQUALIZER_MIDRANGE)
384                     || channelId.equals(CHANNEL_EQUALIZER_TREBLE)) {
385                 if (handleEqualizerCommands(channelId, command, connection, device)) {
386                     waitForUpdate = -1;
387                 }
388             }
389
390             // shuffle command
391             if (channelId.equals(CHANNEL_SHUFFLE)) {
392                 if (command instanceof OnOffType value) {
393                     connection.command(device, "{\"type\":\"ShuffleCommand\",\"shuffle\":\""
394                             + (value == OnOffType.ON ? "true" : "false") + "\"}");
395                 }
396             }
397
398             // play music command
399             if (channelId.equals(CHANNEL_MUSIC_PROVIDER_ID)) {
400                 if (command instanceof StringType) {
401                     waitForUpdate = 0;
402                     String musicProviderId = command.toFullString();
403                     if (!musicProviderId.equals(this.musicProviderId)) {
404                         this.musicProviderId = musicProviderId;
405                         if (this.isPlaying) {
406                             connection.playMusicVoiceCommand(device, this.musicProviderId, "!");
407                             waitForUpdate = 3000;
408                         }
409                     }
410                 }
411             }
412             if (channelId.equals(CHANNEL_PLAY_MUSIC_VOICE_COMMAND)) {
413                 if (command instanceof StringType) {
414                     String voiceCommand = command.toFullString();
415                     if (!this.musicProviderId.isEmpty()) {
416                         connection.playMusicVoiceCommand(device, this.musicProviderId, voiceCommand);
417                         waitForUpdate = 3000;
418                         updatePlayMusicVoiceCommand = true;
419                     }
420                 }
421             }
422
423             // bluetooth commands
424             if (channelId.equals(CHANNEL_BLUETOOTH_MAC)) {
425                 needBluetoothRefresh = true;
426                 if (command instanceof StringType stringCommand) {
427                     String address = stringCommand.toFullString();
428                     if (!address.isEmpty()) {
429                         waitForUpdate = 4000;
430                     }
431                     connection.bluetooth(device, address);
432                 }
433             }
434             if (channelId.equals(CHANNEL_BLUETOOTH)) {
435                 needBluetoothRefresh = true;
436                 if (command == OnOffType.ON) {
437                     waitForUpdate = 4000;
438                     String bluetoothId = lastKnownBluetoothMAC;
439                     BluetoothState state = bluetoothState;
440                     if (state != null && (bluetoothId == null || bluetoothId.isEmpty())) {
441                         for (PairedDevice paired : state.getPairedDeviceList()) {
442                             String pairedAddress = paired.address;
443                             if (pairedAddress != null && !pairedAddress.isEmpty()) {
444                                 lastKnownBluetoothMAC = pairedAddress;
445                                 break;
446                             }
447                         }
448                     }
449                     if (lastKnownBluetoothMAC != null && !lastKnownBluetoothMAC.isEmpty()) {
450                         connection.bluetooth(device, lastKnownBluetoothMAC);
451                     }
452                 } else if (command == OnOffType.OFF) {
453                     connection.bluetooth(device, null);
454                 }
455             }
456             if (channelId.equals(CHANNEL_BLUETOOTH_DEVICE_NAME)) {
457                 needBluetoothRefresh = true;
458             }
459             // amazon music commands
460             if (channelId.equals(CHANNEL_AMAZON_MUSIC_TRACK_ID)) {
461                 if (command instanceof StringType) {
462                     String trackId = command.toFullString();
463                     if (!trackId.isEmpty()) {
464                         waitForUpdate = 3000;
465                     }
466                     connection.playAmazonMusicTrack(device, trackId);
467                 }
468             }
469             if (channelId.equals(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID)) {
470                 if (command instanceof StringType) {
471                     String playListId = command.toFullString();
472                     if (!playListId.isEmpty()) {
473                         waitForUpdate = 3000;
474                     }
475                     connection.playAmazonMusicPlayList(device, playListId);
476                 }
477             }
478             if (channelId.equals(CHANNEL_AMAZON_MUSIC)) {
479                 if (command == OnOffType.ON) {
480                     String lastKnownAmazonMusicId = this.lastKnownAmazonMusicId;
481                     if (lastKnownAmazonMusicId != null && !lastKnownAmazonMusicId.isEmpty()) {
482                         waitForUpdate = 3000;
483                     }
484                     connection.playAmazonMusicTrack(device, lastKnownAmazonMusicId);
485                 } else if (command == OnOffType.OFF) {
486                     connection.playAmazonMusicTrack(device, "");
487                 }
488             }
489
490             // radio commands
491             if (channelId.equals(CHANNEL_RADIO_STATION_ID)) {
492                 if (command instanceof StringType) {
493                     String stationId = command.toFullString();
494                     if (!stationId.isEmpty()) {
495                         waitForUpdate = 3000;
496                     }
497                     connection.playRadio(device, stationId);
498                 }
499             }
500             if (channelId.equals(CHANNEL_RADIO)) {
501                 if (command == OnOffType.ON) {
502                     String lastKnownRadioStationId = this.lastKnownRadioStationId;
503                     if (lastKnownRadioStationId != null && !lastKnownRadioStationId.isEmpty()) {
504                         waitForUpdate = 3000;
505                     }
506                     connection.playRadio(device, lastKnownRadioStationId);
507                 } else if (command == OnOffType.OFF) {
508                     connection.playRadio(device, "");
509                 }
510             }
511
512             // notification
513             if (channelId.equals(CHANNEL_REMIND)) {
514                 if (command instanceof StringType) {
515                     stopCurrentNotification();
516                     String reminder = command.toFullString();
517                     if (!reminder.isEmpty()) {
518                         waitForUpdate = 3000;
519                         updateRemind = true;
520                         currentNotification = connection.notification(device, "Reminder", reminder, null);
521                         currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> {
522                             updateNotificationTimerState();
523                         }, 1, 1, TimeUnit.SECONDS);
524                     }
525                 }
526             }
527             if (channelId.equals(CHANNEL_PLAY_ALARM_SOUND)) {
528                 if (command instanceof StringType) {
529                     stopCurrentNotification();
530                     String alarmSound = command.toFullString();
531                     if (!alarmSound.isEmpty()) {
532                         waitForUpdate = 3000;
533                         updateAlarm = true;
534                         String[] parts = alarmSound.split(":", 2);
535                         JsonNotificationSound sound = new JsonNotificationSound();
536                         if (parts.length == 2) {
537                             sound.providerId = parts[0];
538                             sound.id = parts[1];
539                         } else {
540                             sound.providerId = "ECHO";
541                             sound.id = alarmSound;
542                         }
543                         currentNotification = connection.notification(device, "Alarm", null, sound);
544                         currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> {
545                             updateNotificationTimerState();
546                         }, 1, 1, TimeUnit.SECONDS);
547                     }
548                 }
549             }
550
551             // routine commands
552             if (channelId.equals(CHANNEL_TEXT_TO_SPEECH)) {
553                 if (command instanceof StringType) {
554                     String text = command.toFullString();
555                     if (!text.isEmpty()) {
556                         waitForUpdate = 1000;
557                         updateTextToSpeech = true;
558                         startTextToSpeech(connection, device, text);
559                     }
560                 }
561             }
562             if (channelId.equals(CHANNEL_TEXT_TO_SPEECH_VOLUME)) {
563                 if (command instanceof PercentType percentCommand) {
564                     textToSpeechVolume = percentCommand.intValue();
565                 } else if (command == OnOffType.OFF) {
566                     textToSpeechVolume = 0;
567                 } else if (command == OnOffType.ON) {
568                     textToSpeechVolume = lastKnownVolume;
569                 } else if (command == IncreaseDecreaseType.INCREASE) {
570                     if (textToSpeechVolume < 100) {
571                         textToSpeechVolume++;
572                     }
573                 } else if (command == IncreaseDecreaseType.DECREASE) {
574                     if (textToSpeechVolume > 0) {
575                         textToSpeechVolume--;
576                     }
577                 }
578                 this.updateState(channelId, new PercentType(textToSpeechVolume));
579             }
580             if (channelId.equals(CHANNEL_TEXT_COMMAND)) {
581                 if (command instanceof StringType) {
582                     String text = command.toFullString();
583                     if (!text.isEmpty()) {
584                         waitForUpdate = 1000;
585                         updateTextCommand = true;
586                         startTextCommand(connection, device, text);
587                     }
588                 }
589             }
590             if (channelId.equals(CHANNEL_LAST_VOICE_COMMAND)) {
591                 if (command instanceof StringType) {
592                     String text = command.toFullString();
593                     if (!text.isEmpty()) {
594                         waitForUpdate = -1;
595                         startTextToSpeech(connection, device, text);
596                     }
597                 }
598             }
599             if (channelId.equals(CHANNEL_START_COMMAND)) {
600                 if (command instanceof StringType) {
601                     String commandText = command.toFullString();
602                     if (!commandText.isEmpty()) {
603                         updateStartCommand = true;
604                         if (commandText.startsWith(FLASH_BRIEFING_COMMAND_PREFIX)) {
605                             // Handle custom flashbriefings commands
606                             String flashBriefingId = commandText.substring(FLASH_BRIEFING_COMMAND_PREFIX.length());
607                             for (FlashBriefingProfileHandler flashBriefingHandler : account
608                                     .getFlashBriefingProfileHandlers()) {
609                                 ThingUID flashBriefingUid = flashBriefingHandler.getThing().getUID();
610                                 if (flashBriefingId.equals(flashBriefingHandler.getThing().getUID().getId())) {
611                                     flashBriefingHandler.handleCommand(
612                                             new ChannelUID(flashBriefingUid, CHANNEL_PLAY_ON_DEVICE),
613                                             new StringType(device.serialNumber));
614                                     break;
615                                 }
616                             }
617                         } else {
618                             // Handle standard commands
619                             if (!commandText.startsWith("Alexa.")) {
620                                 commandText = "Alexa." + commandText + ".Play";
621                             }
622                             waitForUpdate = 1000;
623                             connection.executeSequenceCommand(device, commandText, Map.of());
624                         }
625                     }
626                 }
627             }
628             if (channelId.equals(CHANNEL_START_ROUTINE)) {
629                 if (command instanceof StringType) {
630                     String utterance = command.toFullString();
631                     if (!utterance.isEmpty()) {
632                         waitForUpdate = 1000;
633                         updateRoutine = true;
634                         connection.startRoutine(device, utterance);
635                     }
636                 }
637             }
638             if (waitForUpdate < 0) {
639                 return;
640             }
641             // force update of the state
642             this.disableUpdate = true;
643             final boolean bluetoothRefresh = needBluetoothRefresh;
644             Runnable doRefresh = () -> {
645                 this.disableUpdate = false;
646                 BluetoothState state = null;
647                 if (bluetoothRefresh) {
648                     JsonBluetoothStates states;
649                     states = connection.getBluetoothConnectionStates();
650                     if (states != null) {
651                         state = states.findStateByDevice(device);
652                     }
653                 }
654
655                 updateState(account, device, state, null, null, null, null, null);
656             };
657             if (command instanceof RefreshType) {
658                 waitForUpdate = 0;
659                 account.forceCheckData();
660             }
661             if (waitForUpdate == 0) {
662                 doRefresh.run();
663             } else {
664                 this.updateStateJob = scheduler.schedule(doRefresh, waitForUpdate, TimeUnit.MILLISECONDS);
665             }
666         } catch (IOException | URISyntaxException | InterruptedException e) {
667             logger.info("handleCommand fails", e);
668         }
669     }
670
671     private boolean handleEqualizerCommands(String channelId, Command command, Connection connection, Device device)
672             throws URISyntaxException {
673         if (command instanceof RefreshType) {
674             this.lastKnownEqualizer = null;
675         }
676         if (command instanceof DecimalType decimalCommand) {
677             if (this.lastKnownEqualizer == null) {
678                 updateEqualizerState();
679             }
680             JsonEqualizer lastKnownEqualizer = this.lastKnownEqualizer;
681             if (lastKnownEqualizer != null) {
682                 JsonEqualizer newEqualizerSetting = lastKnownEqualizer.createClone();
683                 if (channelId.equals(CHANNEL_EQUALIZER_BASS)) {
684                     newEqualizerSetting.bass = decimalCommand.intValue();
685                 }
686                 if (channelId.equals(CHANNEL_EQUALIZER_MIDRANGE)) {
687                     newEqualizerSetting.mid = decimalCommand.intValue();
688                 }
689                 if (channelId.equals(CHANNEL_EQUALIZER_TREBLE)) {
690                     newEqualizerSetting.treble = decimalCommand.intValue();
691                 }
692                 try {
693                     connection.setEqualizer(device, newEqualizerSetting);
694                     return true;
695                 } catch (HttpException | IOException | ConnectionException | InterruptedException e) {
696                     logger.debug("Update equalizer failed", e);
697                     this.lastKnownEqualizer = null;
698                 }
699             }
700         }
701         return false;
702     }
703
704     private void startTextToSpeech(Connection connection, Device device, String text)
705             throws IOException, URISyntaxException {
706         Integer volume = null;
707         if (textToSpeechVolume != 0) {
708             volume = textToSpeechVolume;
709         }
710         connection.textToSpeech(device, text, volume, lastKnownVolume);
711     }
712
713     private void startTextCommand(Connection connection, Device device, String text)
714             throws IOException, URISyntaxException {
715         Integer volume = null;
716         if (textToSpeechVolume != 0) {
717             volume = textToSpeechVolume;
718         }
719         connection.textCommand(device, text, volume, lastKnownVolume);
720     }
721
722     @Override
723     public void startAnnouncement(Device device, String speak, String bodyText, @Nullable String title,
724             @Nullable Integer volume) throws IOException, URISyntaxException {
725         Connection connection = this.findConnection();
726         if (connection == null) {
727             return;
728         }
729         if (volume == null && textToSpeechVolume != 0) {
730             volume = textToSpeechVolume;
731         }
732         if (volume != null && volume < 0) {
733             volume = null; // the meaning of negative values is 'do not use'. The api requires null in this case.
734         }
735         connection.announcement(device, speak, bodyText, title, volume, lastKnownVolume);
736     }
737
738     private void stopCurrentNotification() {
739         ScheduledFuture<?> currentNotifcationUpdateTimer = this.currentNotifcationUpdateTimer;
740         if (currentNotifcationUpdateTimer != null) {
741             this.currentNotifcationUpdateTimer = null;
742             currentNotifcationUpdateTimer.cancel(true);
743         }
744         JsonNotificationResponse currentNotification = this.currentNotification;
745         if (currentNotification != null) {
746             this.currentNotification = null;
747             Connection currentConnection = this.findConnection();
748             if (currentConnection != null) {
749                 try {
750                     currentConnection.stopNotification(currentNotification);
751                 } catch (IOException | URISyntaxException | InterruptedException e) {
752                     logger.warn("Stop notification failed", e);
753                 }
754             }
755         }
756     }
757
758     private void updateNotificationTimerState() {
759         boolean stopCurrentNotification = true;
760         JsonNotificationResponse currentNotification = this.currentNotification;
761         try {
762             if (currentNotification != null) {
763                 Connection currentConnection = this.findConnection();
764                 if (currentConnection != null) {
765                     JsonNotificationResponse newState = currentConnection.getNotificationState(currentNotification);
766                     if (newState != null && "ON".equals(newState.status)) {
767                         stopCurrentNotification = false;
768                     }
769                 }
770             }
771         } catch (IOException | URISyntaxException | InterruptedException e) {
772             logger.warn("update notification state fails", e);
773         }
774         if (stopCurrentNotification) {
775             if (currentNotification != null) {
776                 String type = currentNotification.type;
777                 if (type != null) {
778                     if ("Reminder".equals(type)) {
779                         updateState(CHANNEL_REMIND, StringType.EMPTY);
780                         updateRemind = false;
781                     }
782                     if ("Alarm".equals(type)) {
783                         updateState(CHANNEL_PLAY_ALARM_SOUND, StringType.EMPTY);
784                         updateAlarm = false;
785                     }
786                 }
787             }
788             stopCurrentNotification();
789         }
790     }
791
792     public void updateState(AccountHandler accountHandler, @Nullable Device device,
793             @Nullable BluetoothState bluetoothState, @Nullable DeviceNotificationState deviceNotificationState,
794             @Nullable AscendingAlarmModel ascendingAlarmModel, @Nullable JsonPlaylists playlists,
795             @Nullable List<JsonNotificationSound> alarmSounds, @Nullable List<JsonMusicProvider> musicProviders) {
796         try {
797             this.logger.debug("Handle updateState {}", this.getThing().getUID());
798
799             if (deviceNotificationState != null) {
800                 notificationVolumeLevel = deviceNotificationState.volumeLevel;
801             }
802             if (ascendingAlarmModel != null) {
803                 ascendingAlarm = ascendingAlarmModel.ascendingAlarmEnabled;
804             }
805             if (playlists != null) {
806                 this.playLists = playlists;
807             }
808             if (alarmSounds != null) {
809                 this.alarmSounds = alarmSounds;
810             }
811             if (musicProviders != null) {
812                 this.musicProviders = musicProviders;
813             }
814             if (!setDeviceAndUpdateThingState(accountHandler, device, null)) {
815                 this.logger.debug("Handle updateState {} aborted: Not online", this.getThing().getUID());
816                 return;
817             }
818             if (device == null) {
819                 this.logger.debug("Handle updateState {} aborted: No device", this.getThing().getUID());
820                 return;
821             }
822
823             if (this.disableUpdate) {
824                 this.logger.debug("Handle updateState {} aborted: Disabled", this.getThing().getUID());
825                 return;
826             }
827             Connection connection = this.findConnection();
828             if (connection == null) {
829                 return;
830             }
831
832             if (this.lastKnownEqualizer == null) {
833                 updateEqualizerState();
834             }
835
836             PlayerInfo playerInfo = null;
837             Provider provider = null;
838             InfoText infoText = null;
839             MainArt mainArt = null;
840             String musicProviderId = null;
841             Progress progress = null;
842             try {
843                 JsonPlayerState playerState = connection.getPlayer(device);
844                 if (playerState != null) {
845                     playerInfo = playerState.playerInfo;
846                     if (playerInfo != null) {
847                         infoText = playerInfo.infoText;
848                         if (infoText == null) {
849                             infoText = playerInfo.miniInfoText;
850                         }
851                         mainArt = playerInfo.mainArt;
852                         provider = playerInfo.provider;
853                         if (provider != null) {
854                             musicProviderId = provider.providerName;
855                             // Map the music provider id to the one used for starting music with voice command
856                             if (musicProviderId != null) {
857                                 musicProviderId = musicProviderId.toUpperCase();
858
859                                 if ("AMAZON MUSIC".equals(musicProviderId)) {
860                                     musicProviderId = "AMAZON_MUSIC";
861                                 }
862                                 if ("CLOUD_PLAYER".equals(musicProviderId)) {
863                                     musicProviderId = "AMAZON_MUSIC";
864                                 }
865                                 if (musicProviderId.startsWith("TUNEIN")) {
866                                     musicProviderId = "TUNEIN";
867                                 }
868                                 if (musicProviderId.startsWith("IHEARTRADIO")) {
869                                     musicProviderId = "I_HEART_RADIO";
870                                 }
871                                 if ("APPLE".equals(musicProviderId) && musicProviderId.contains("MUSIC")) {
872                                     musicProviderId = "APPLE_MUSIC";
873                                 }
874                             }
875                         }
876                         progress = playerInfo.progress;
877                     }
878                 }
879             } catch (HttpException e) {
880                 if (e.getCode() != 400) {
881                     logger.info("getPlayer fails", e);
882                 }
883             } catch (IOException | URISyntaxException | InterruptedException e) {
884                 logger.info("getPlayer fails", e);
885             }
886             // check playing
887             isPlaying = (playerInfo != null && "PLAYING".equals(playerInfo.state));
888
889             isPaused = (playerInfo != null && "PAUSED".equals(playerInfo.state));
890             synchronized (progressLock) {
891                 Boolean showTime = null;
892                 Long mediaLength = null;
893                 Long mediaProgress = null;
894                 if (progress != null) {
895                     showTime = progress.showTiming;
896                     mediaLength = progress.mediaLength;
897                     mediaProgress = progress.mediaProgress;
898                 }
899                 if (showTime != null && showTime && mediaProgress != null && mediaLength != null) {
900                     mediaProgressMs = mediaProgress * 1000;
901                     mediaLengthMs = mediaLength * 1000;
902                     mediaStartMs = System.currentTimeMillis() - mediaProgressMs;
903                     if (isPlaying) {
904                         if (updateProgressJob == null) {
905                             updateProgressJob = scheduler.scheduleWithFixedDelay(this::updateMediaProgress, 1000, 1000,
906                                     TimeUnit.MILLISECONDS);
907                         }
908                     } else {
909                         stopProgressTimer();
910                     }
911                 } else {
912                     stopProgressTimer();
913                     mediaProgressMs = 0;
914                     mediaStartMs = 0;
915                     mediaLengthMs = 0;
916                 }
917                 updateMediaProgress(true);
918             }
919
920             JsonMediaState mediaState = null;
921             try {
922                 if ("AMAZON_MUSIC".equalsIgnoreCase(musicProviderId) || "TUNEIN".equalsIgnoreCase(musicProviderId)) {
923                     mediaState = connection.getMediaState(device);
924                 }
925             } catch (HttpException e) {
926                 if (e.getCode() == 400) {
927                     updateState(CHANNEL_RADIO_STATION_ID, StringType.EMPTY);
928                 } else {
929                     logger.info("getMediaState fails", e);
930                 }
931             } catch (IOException | URISyntaxException | InterruptedException e) {
932                 logger.info("getMediaState fails", e);
933             }
934
935             // handle music provider id
936             if (provider != null && isPlaying) {
937                 if (musicProviderId != null) {
938                     this.musicProviderId = musicProviderId;
939                 }
940             }
941
942             // handle amazon music
943             String amazonMusicTrackId = "";
944             String amazonMusicPlayListId = "";
945             boolean amazonMusic = false;
946             if (mediaState != null) {
947                 String contentId = mediaState.contentId;
948                 if (isPlaying && "CLOUD_PLAYER".equals(mediaState.providerId) && contentId != null
949                         && !contentId.isEmpty()) {
950                     amazonMusicTrackId = contentId;
951                     lastKnownAmazonMusicId = amazonMusicTrackId;
952                     amazonMusic = true;
953                 }
954             }
955
956             // handle bluetooth
957             String bluetoothMAC = "";
958             String bluetoothDeviceName = "";
959             boolean bluetoothIsConnected = false;
960             if (bluetoothState != null) {
961                 this.bluetoothState = bluetoothState;
962                 for (PairedDevice paired : bluetoothState.getPairedDeviceList()) {
963                     String pairedAddress = paired.address;
964                     if (paired.connected && pairedAddress != null) {
965                         bluetoothIsConnected = true;
966                         bluetoothMAC = pairedAddress;
967                         bluetoothDeviceName = paired.friendlyName;
968                         if (bluetoothDeviceName == null || bluetoothDeviceName.isEmpty()) {
969                             bluetoothDeviceName = pairedAddress;
970                         }
971                         break;
972                     }
973                 }
974
975             }
976             if (!bluetoothMAC.isEmpty()) {
977                 lastKnownBluetoothMAC = bluetoothMAC;
978             }
979
980             // handle radio
981             boolean isRadio = false;
982             String radioStationId = "";
983             if (mediaState != null) {
984                 radioStationId = Objects.requireNonNullElse(mediaState.radioStationId, "");
985                 if (!radioStationId.isEmpty()) {
986                     lastKnownRadioStationId = radioStationId;
987                     if ("TUNEIN".equalsIgnoreCase(musicProviderId)) {
988                         isRadio = true;
989                         if (!"PLAYING".equals(mediaState.currentState)) {
990                             radioStationId = "";
991                         }
992                     }
993                 }
994             }
995
996             // handle title, subtitle, imageUrl
997             String title = "";
998             String subTitle1 = "";
999             String subTitle2 = "";
1000             String imageUrl = "";
1001             if (infoText != null) {
1002                 if (infoText.title != null) {
1003                     title = infoText.title;
1004                 }
1005                 if (infoText.subText1 != null) {
1006                     subTitle1 = infoText.subText1;
1007                 }
1008
1009                 if (infoText.subText2 != null) {
1010                     subTitle2 = infoText.subText2;
1011                 }
1012             }
1013             if (mainArt != null) {
1014                 if (mainArt.url != null) {
1015                     imageUrl = mainArt.url;
1016                 }
1017             }
1018             if (mediaState != null) {
1019                 List<QueueEntry> queueEntries = Objects.requireNonNullElse(mediaState.queue, List.of());
1020                 if (!queueEntries.isEmpty()) {
1021                     QueueEntry entry = queueEntries.get(0);
1022                     if (isRadio) {
1023                         if ((imageUrl == null || imageUrl.isEmpty()) && entry.imageURL != null) {
1024                             imageUrl = entry.imageURL;
1025                         }
1026                         if ((subTitle1 == null || subTitle1.isEmpty()) && entry.radioStationSlogan != null) {
1027                             subTitle1 = entry.radioStationSlogan;
1028                         }
1029                         if ((subTitle2 == null || subTitle2.isEmpty()) && entry.radioStationLocation != null) {
1030                             subTitle2 = entry.radioStationLocation;
1031                         }
1032                     }
1033
1034                 }
1035             }
1036
1037             // handle provider
1038             String providerDisplayName = "";
1039             if (provider != null) {
1040                 if (provider.providerDisplayName != null) {
1041                     providerDisplayName = Objects.requireNonNullElse(provider.providerDisplayName, providerDisplayName);
1042                 }
1043                 String providerName = provider.providerName;
1044                 if (providerName != null && !providerName.isEmpty() && providerDisplayName.isEmpty()) {
1045                     providerDisplayName = provider.providerName;
1046                 }
1047             }
1048
1049             // handle volume
1050             Integer volume = null;
1051             if (!connection.isSequenceNodeQueueRunning()) {
1052                 if (mediaState != null) {
1053                     volume = mediaState.volume;
1054                 }
1055                 if (playerInfo != null && volume == null) {
1056                     Volume volumnInfo = playerInfo.volume;
1057                     if (volumnInfo != null) {
1058                         volume = volumnInfo.volume;
1059                     }
1060                 }
1061                 if (volume != null && volume > 0) {
1062                     lastKnownVolume = volume;
1063                 }
1064                 if (volume == null) {
1065                     volume = lastKnownVolume;
1066                 }
1067             }
1068             // Update states
1069             if (updateRemind && currentNotifcationUpdateTimer == null) {
1070                 updateRemind = false;
1071                 updateState(CHANNEL_REMIND, StringType.EMPTY);
1072             }
1073             if (updateAlarm && currentNotifcationUpdateTimer == null) {
1074                 updateAlarm = false;
1075                 updateState(CHANNEL_PLAY_ALARM_SOUND, StringType.EMPTY);
1076             }
1077             if (updateRoutine) {
1078                 updateRoutine = false;
1079                 updateState(CHANNEL_START_ROUTINE, StringType.EMPTY);
1080             }
1081             if (updateTextToSpeech) {
1082                 updateTextToSpeech = false;
1083                 updateState(CHANNEL_TEXT_TO_SPEECH, StringType.EMPTY);
1084             }
1085             if (updateTextCommand) {
1086                 updateTextCommand = false;
1087                 updateState(CHANNEL_TEXT_COMMAND, StringType.EMPTY);
1088             }
1089             if (updatePlayMusicVoiceCommand) {
1090                 updatePlayMusicVoiceCommand = false;
1091                 updateState(CHANNEL_PLAY_MUSIC_VOICE_COMMAND, StringType.EMPTY);
1092             }
1093             if (updateStartCommand) {
1094                 updateStartCommand = false;
1095                 updateState(CHANNEL_START_COMMAND, StringType.EMPTY);
1096             }
1097
1098             updateState(CHANNEL_MUSIC_PROVIDER_ID, new StringType(musicProviderId));
1099             updateState(CHANNEL_AMAZON_MUSIC_TRACK_ID, new StringType(amazonMusicTrackId));
1100             updateState(CHANNEL_AMAZON_MUSIC, OnOffType.from(isPlaying && amazonMusic));
1101             updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID, new StringType(amazonMusicPlayListId));
1102             updateState(CHANNEL_RADIO_STATION_ID, new StringType(radioStationId));
1103             updateState(CHANNEL_RADIO, OnOffType.from(isPlaying && isRadio));
1104             updateState(CHANNEL_PROVIDER_DISPLAY_NAME, new StringType(providerDisplayName));
1105             updateState(CHANNEL_PLAYER, isPlaying ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
1106             updateState(CHANNEL_IMAGE_URL, new StringType(imageUrl));
1107             updateState(CHANNEL_TITLE, new StringType(title));
1108             if (volume != null) {
1109                 updateState(CHANNEL_VOLUME, new PercentType(volume));
1110             }
1111             updateState(CHANNEL_SUBTITLE1, new StringType(subTitle1));
1112             updateState(CHANNEL_SUBTITLE2, new StringType(subTitle2));
1113             if (bluetoothState != null) {
1114                 updateState(CHANNEL_BLUETOOTH, OnOffType.from(bluetoothIsConnected));
1115                 updateState(CHANNEL_BLUETOOTH_MAC, new StringType(bluetoothMAC));
1116                 updateState(CHANNEL_BLUETOOTH_DEVICE_NAME, new StringType(bluetoothDeviceName));
1117             }
1118
1119             updateState(CHANNEL_ASCENDING_ALARM,
1120                     ascendingAlarm != null ? OnOffType.from(ascendingAlarm) : UnDefType.UNDEF);
1121
1122             final Integer notificationVolumeLevel = this.notificationVolumeLevel;
1123             if (notificationVolumeLevel != null) {
1124                 updateState(CHANNEL_NOTIFICATION_VOLUME, new PercentType(notificationVolumeLevel));
1125             } else {
1126                 updateState(CHANNEL_NOTIFICATION_VOLUME, UnDefType.UNDEF);
1127             }
1128         } catch (Exception e) {
1129             this.logger.debug("Handle updateState {} failed: {}", this.getThing().getUID(), e.getMessage(), e);
1130
1131             disableUpdate = false;
1132             throw e; // Rethrow same exception
1133         }
1134     }
1135
1136     private void updateEqualizerState() {
1137         if (!this.capabilities.contains("SOUND_SETTINGS")) {
1138             return;
1139         }
1140
1141         Connection connection = findConnection();
1142         if (connection == null) {
1143             return;
1144         }
1145         Device device = findDevice();
1146         if (device == null) {
1147             return;
1148         }
1149         Integer bass = null;
1150         Integer midrange = null;
1151         Integer treble = null;
1152         try {
1153             JsonEqualizer equalizer = connection.getEqualizer(device);
1154             if (equalizer != null) {
1155                 bass = equalizer.bass;
1156                 midrange = equalizer.mid;
1157                 treble = equalizer.treble;
1158             }
1159             this.lastKnownEqualizer = equalizer;
1160         } catch (IOException | URISyntaxException | HttpException | ConnectionException | InterruptedException e) {
1161             logger.debug("Get equalizer failes", e);
1162             return;
1163         }
1164         if (bass != null) {
1165             updateState(CHANNEL_EQUALIZER_BASS, new DecimalType(bass));
1166         }
1167         if (midrange != null) {
1168             updateState(CHANNEL_EQUALIZER_MIDRANGE, new DecimalType(midrange));
1169         }
1170         if (treble != null) {
1171             updateState(CHANNEL_EQUALIZER_TREBLE, new DecimalType(treble));
1172         }
1173     }
1174
1175     private void updateMediaProgress() {
1176         updateMediaProgress(false);
1177     }
1178
1179     private void updateMediaProgress(boolean updateMediaLength) {
1180         synchronized (progressLock) {
1181             if (mediaStartMs > 0) {
1182                 long currentPlayTimeMs = isPlaying ? System.currentTimeMillis() - mediaStartMs : mediaProgressMs;
1183                 if (mediaLengthMs > 0) {
1184                     int progressPercent = (int) Math.min(100,
1185                             Math.round((double) currentPlayTimeMs / (double) mediaLengthMs * 100));
1186                     updateState(CHANNEL_MEDIA_PROGRESS, new PercentType(progressPercent));
1187                 } else {
1188                     updateState(CHANNEL_MEDIA_PROGRESS, UnDefType.UNDEF);
1189                 }
1190                 updateState(CHANNEL_MEDIA_PROGRESS_TIME, new QuantityType<>(currentPlayTimeMs / 1000, Units.SECOND));
1191                 if (updateMediaLength) {
1192                     updateState(CHANNEL_MEDIA_LENGTH, new QuantityType<>(mediaLengthMs / 1000, Units.SECOND));
1193                 }
1194             } else {
1195                 updateState(CHANNEL_MEDIA_PROGRESS, UnDefType.UNDEF);
1196                 updateState(CHANNEL_MEDIA_LENGTH, UnDefType.UNDEF);
1197                 updateState(CHANNEL_MEDIA_PROGRESS_TIME, UnDefType.UNDEF);
1198                 if (updateMediaLength) {
1199                     updateState(CHANNEL_MEDIA_LENGTH, UnDefType.UNDEF);
1200                 }
1201             }
1202         }
1203     }
1204
1205     public void handlePushActivity(Activity pushActivity) {
1206         if ("DISCARDED_NON_DEVICE_DIRECTED_INTENT".equals(pushActivity.activityStatus)) {
1207             return;
1208         }
1209         Description description = pushActivity.parseDescription();
1210         String firstUtteranceId = description.firstUtteranceId;
1211         if (firstUtteranceId == null || firstUtteranceId.isEmpty()
1212                 || firstUtteranceId.toLowerCase().startsWith("textclient:")) {
1213             return;
1214         }
1215         String firstStreamId = description.firstStreamId;
1216         if (firstStreamId == null || firstStreamId.isEmpty()) {
1217             return;
1218         }
1219         String spokenText = description.summary;
1220         if (spokenText != null && !spokenText.isEmpty()) {
1221             // remove wake word
1222             String wakeWordPrefix = this.wakeWord;
1223             if (wakeWordPrefix != null) {
1224                 wakeWordPrefix += " ";
1225                 if (spokenText.toLowerCase().startsWith(wakeWordPrefix.toLowerCase())) {
1226                     spokenText = spokenText.substring(wakeWordPrefix.length());
1227                 }
1228             }
1229
1230             if (lastSpokenText.isEmpty() || lastSpokenText.equals(spokenText)) {
1231                 updateState(CHANNEL_LAST_VOICE_COMMAND, StringType.EMPTY);
1232             }
1233             lastSpokenText = spokenText;
1234             updateState(CHANNEL_LAST_VOICE_COMMAND, new StringType(spokenText));
1235         }
1236     }
1237
1238     public void handlePushCommand(String command, String payload) {
1239         this.logger.debug("Handle push command {}", command);
1240         switch (command) {
1241             case "PUSH_VOLUME_CHANGE":
1242                 JsonCommandPayloadPushVolumeChange volumeChange = Objects
1243                         .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushVolumeChange.class));
1244                 Connection connection = this.findConnection();
1245                 Integer volumeSetting = volumeChange.volumeSetting;
1246                 Boolean muted = volumeChange.isMuted;
1247                 if (muted != null && muted) {
1248                     updateState(CHANNEL_VOLUME, new PercentType(0));
1249                 }
1250                 if (volumeSetting != null && connection != null && !connection.isSequenceNodeQueueRunning()) {
1251                     lastKnownVolume = volumeSetting;
1252                     updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume));
1253                 }
1254                 break;
1255             case "PUSH_EQUALIZER_STATE_CHANGE":
1256                 updateEqualizerState();
1257                 break;
1258             default:
1259                 AccountHandler account = this.account;
1260                 Device device = this.device;
1261                 if (account != null && device != null) {
1262                     this.disableUpdate = false;
1263                     updateState(account, device, null, null, null, null, null, null);
1264                 }
1265         }
1266     }
1267
1268     public void updateNotifications(ZonedDateTime currentTime, ZonedDateTime now,
1269             @Nullable JsonCommandPayloadPushNotificationChange pushPayload,
1270             List<JsonNotificationResponse> notifications) {
1271         Device device = this.device;
1272         if (device == null) {
1273             return;
1274         }
1275
1276         ZonedDateTime nextReminder = null;
1277         ZonedDateTime nextAlarm = null;
1278         ZonedDateTime nextMusicAlarm = null;
1279         ZonedDateTime nextTimer = null;
1280         for (JsonNotificationResponse notification : notifications) {
1281             if (Objects.equals(notification.deviceSerialNumber, device.serialNumber)) {
1282                 // notification for this device
1283                 if ("ON".equals(notification.status)) {
1284                     if ("Reminder".equals(notification.type)) {
1285                         String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1286                         String date = notification.originalDate != null ? notification.originalDate
1287                                 : ZonedDateTime.now().toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
1288                         String time = notification.originalTime != null ? notification.originalTime : "00:00:00";
1289                         ZonedDateTime alarmTime = ZonedDateTime.parse(date + "T" + time + offset,
1290                                 DateTimeFormatter.ISO_DATE_TIME);
1291                         String recurringPattern = notification.recurringPattern;
1292                         if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
1293                             continue; // Ignore recurring entry if alarm time is before now
1294                         }
1295                         if (nextReminder == null || alarmTime.isBefore(nextReminder)) {
1296                             nextReminder = alarmTime;
1297                         }
1298                     } else if ("Timer".equals(notification.type)) {
1299                         // use remaining time
1300                         ZonedDateTime alarmTime = currentTime.plus(notification.remainingTime, ChronoUnit.MILLIS);
1301                         if (nextTimer == null || alarmTime.isBefore(nextTimer)) {
1302                             nextTimer = alarmTime;
1303                         }
1304                     } else if ("Alarm".equals(notification.type)) {
1305                         String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1306                         ZonedDateTime alarmTime = ZonedDateTime
1307                                 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1308                         String recurringPattern = notification.recurringPattern;
1309                         if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
1310                             continue; // Ignore recurring entry if alarm time is before now
1311                         }
1312                         if (nextAlarm == null || alarmTime.isBefore(nextAlarm)) {
1313                             nextAlarm = alarmTime;
1314                         }
1315                     } else if ("MusicAlarm".equals(notification.type)) {
1316                         String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1317                         ZonedDateTime alarmTime = ZonedDateTime
1318                                 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1319                         String recurringPattern = notification.recurringPattern;
1320                         if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
1321                             continue; // Ignore recurring entry if alarm time is before now
1322                         }
1323                         if (nextMusicAlarm == null || alarmTime.isBefore(nextMusicAlarm)) {
1324                             nextMusicAlarm = alarmTime;
1325                         }
1326                     }
1327                 }
1328             }
1329         }
1330
1331         updateState(CHANNEL_NEXT_REMINDER, nextReminder == null ? UnDefType.UNDEF : new DateTimeType(nextReminder));
1332         updateState(CHANNEL_NEXT_ALARM, nextAlarm == null ? UnDefType.UNDEF : new DateTimeType(nextAlarm));
1333         updateState(CHANNEL_NEXT_MUSIC_ALARM,
1334                 nextMusicAlarm == null ? UnDefType.UNDEF : new DateTimeType(nextMusicAlarm));
1335         updateState(CHANNEL_NEXT_TIMER, nextTimer == null ? UnDefType.UNDEF : new DateTimeType(nextTimer));
1336     }
1337
1338     @Override
1339     public void updateChannelState(String channelId, State state) {
1340         updateState(channelId, state);
1341     }
1342 }