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