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