2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.amazonechocontrol.internal.handler;
15 import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*;
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;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.stream.Collectors;
27 import java.util.stream.Stream;
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.Units;
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;
85 import com.google.gson.Gson;
88 * The {@link EchoHandler} is responsible for the handling of the echo device
90 * @author Michael Geramb - Initial contribution
93 public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
94 private final Logger logger = LoggerFactory.getLogger(EchoHandler.class);
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<>();
127 private @Nullable JsonNotificationResponse currentNotification;
128 private @Nullable ScheduledFuture<?> currentNotifcationUpdateTimer;
130 long mediaProgressMs;
132 String lastSpokenText = "";
134 public EchoHandler(Thing thing, Gson gson) {
137 channelHandlers.add(new ChannelHandlerAnnouncement(this, this.gson));
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);
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;
159 if (device == null) {
160 updateStatus(ThingStatus.UNKNOWN);
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());
168 if (!device.online) {
169 updateStatus(ThingStatus.OFFLINE);
172 updateStatus(ThingStatus.ONLINE);
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);
189 private void stopProgressTimer() {
190 ScheduledFuture<?> updateProgressJob = this.updateProgressJob;
191 this.updateProgressJob = null;
192 if (updateProgressJob != null) {
193 updateProgressJob.cancel(false);
197 public @Nullable BluetoothState findBluetoothState() {
198 return this.bluetoothState;
201 public @Nullable JsonPlaylists findPlaylists() {
202 return this.playLists;
205 public @Nullable JsonNotificationSound @Nullable [] findAlarmSounds() {
206 return this.alarmSounds;
209 public @Nullable List<JsonMusicProvider> findMusicProviders() {
210 return this.musicProviders;
213 private @Nullable Connection findConnection() {
214 AccountHandler accountHandler = this.account;
215 if (accountHandler != null) {
216 return accountHandler.findConnection();
221 public @Nullable AccountHandler findAccount() {
225 public @Nullable Device findDevice() {
229 public String findSerialNumber() {
230 String id = (String) getConfig().get(DEVICE_PROPERTY_SERIAL_NUMBER);
238 public void handleCommand(ChannelUID channelUID, Command command) {
240 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
241 int waitForUpdate = 1000;
242 boolean needBluetoothRefresh = false;
243 String lastKnownBluetoothMAC = this.lastKnownBluetoothMAC;
245 ScheduledFuture<?> updateStateJob = this.updateStateJob;
246 this.updateStateJob = null;
247 if (updateStateJob != null) {
248 this.disableUpdate = false;
249 updateStateJob.cancel(false);
251 AccountHandler account = this.account;
252 if (account == null) {
255 Connection connection = account.findConnection();
256 if (connection == null) {
259 Device device = this.device;
260 if (device == null) {
264 String channelId = channelUID.getId();
265 for (ChannelHandler channelHandler : channelHandlers) {
266 if (channelHandler.tryHandleCommand(device, connection, channelId, command)) {
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) {
277 connection.command(device, "{\"type\":\"PlayCommand\"}");
279 connection.playMusicVoiceCommand(device, this.musicProviderId, "!");
280 waitForUpdate = 3000;
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\"}");
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;
299 account.forceCheckData();
302 if (channelId.equals(CHANNEL_ASCENDING_ALARM)) {
303 if (command == OnOffType.OFF) {
304 connection.ascendingAlarm(device, false);
305 this.ascendingAlarm = false;
307 account.forceCheckData();
309 if (command == OnOffType.ON) {
310 connection.ascendingAlarm(device, true);
311 this.ascendingAlarm = true;
313 account.forceCheckData();
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));
325 if (channelId.equals(CHANNEL_MEDIA_PROGRESS_TIME)) {
326 if (command instanceof DecimalType) {
327 DecimalType value = (DecimalType) command;
328 mediaPosition = value.longValue();
330 if (command instanceof QuantityType<?>) {
331 QuantityType<?> value = (QuantityType<?>) command;
333 QuantityType<?> seconds = value.toUnit(Units.SECOND);
334 if (seconds != null) {
335 mediaPosition = seconds.longValue();
339 if (mediaPosition != null) {
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);
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) {
359 } else if (command == OnOffType.ON) {
360 volume = lastKnownVolume;
361 } else if (command == IncreaseDecreaseType.INCREASE) {
362 if (lastKnownVolume < 100) {
364 volume = lastKnownVolume;
366 } else if (command == IncreaseDecreaseType.DECREASE) {
367 if (lastKnownVolume > 0) {
369 volume = lastKnownVolume;
372 if (volume != null) {
373 if ("WHA".equals(device.deviceFamily)) {
374 connection.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + volume
375 + ",\"contentFocusClientId\":\"Default\"}");
377 connection.volume(device, volume);
379 lastKnownVolume = volume;
380 updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume));
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)) {
393 if (channelId.equals(CHANNEL_SHUFFLE)) {
394 if (command instanceof OnOffType) {
395 OnOffType value = (OnOffType) command;
397 connection.command(device, "{\"type\":\"ShuffleCommand\",\"shuffle\":\""
398 + (value == OnOffType.ON ? "true" : "false") + "\"}");
402 // play music command
403 if (channelId.equals(CHANNEL_MUSIC_PROVIDER_ID)) {
404 if (command instanceof StringType) {
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;
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;
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;
435 connection.bluetooth(device, address);
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) {
451 String pairedAddress = paired.address;
452 if (pairedAddress != null && !pairedAddress.isEmpty()) {
453 lastKnownBluetoothMAC = pairedAddress;
459 if (lastKnownBluetoothMAC != null && !lastKnownBluetoothMAC.isEmpty()) {
460 connection.bluetooth(device, lastKnownBluetoothMAC);
462 } else if (command == OnOffType.OFF) {
463 connection.bluetooth(device, null);
466 if (channelId.equals(CHANNEL_BLUETOOTH_DEVICE_NAME)) {
467 needBluetoothRefresh = true;
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;
476 connection.playAmazonMusicTrack(device, trackId);
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;
485 connection.playAmazonMusicPlayList(device, playListId);
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;
494 connection.playAmazonMusicTrack(device, lastKnownAmazonMusicId);
495 } else if (command == OnOffType.OFF) {
496 connection.playAmazonMusicTrack(device, "");
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;
507 connection.playRadio(device, stationId);
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;
516 connection.playRadio(device, lastKnownRadioStationId);
517 } else if (command == OnOffType.OFF) {
518 connection.playRadio(device, "");
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;
530 currentNotification = connection.notification(device, "Reminder", reminder, null);
531 currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> {
532 updateNotificationTimerState();
533 }, 1, 1, TimeUnit.SECONDS);
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;
544 String[] parts = alarmSound.split(":", 2);
545 JsonNotificationSound sound = new JsonNotificationSound();
546 if (parts.length == 2) {
547 sound.providerId = parts[0];
550 sound.providerId = "ECHO";
551 sound.id = alarmSound;
553 currentNotification = connection.notification(device, "Alarm", null, sound);
554 currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> {
555 updateNotificationTimerState();
556 }, 1, 1, TimeUnit.SECONDS);
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);
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++;
584 } else if (command == IncreaseDecreaseType.DECREASE) {
585 if (textToSpeechVolume > 0) {
586 textToSpeechVolume--;
589 this.updateState(channelId, new PercentType(textToSpeechVolume));
591 if (channelId.equals(CHANNEL_LAST_VOICE_COMMAND)) {
592 if (command instanceof StringType) {
593 String text = command.toFullString();
594 if (!text.isEmpty()) {
596 startTextToSpeech(connection, device, text);
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));
619 // Handle standard commands
620 if (!commandText.startsWith("Alexa.")) {
621 commandText = "Alexa." + commandText + ".Play";
623 waitForUpdate = 1000;
624 connection.executeSequenceCommand(device, commandText, Map.of());
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);
639 if (waitForUpdate < 0) {
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);
656 updateState(account, device, state, null, null, null, null, null);
658 if (command instanceof RefreshType) {
660 account.forceCheckData();
662 if (waitForUpdate == 0) {
665 this.updateStateJob = scheduler.schedule(doRefresh, waitForUpdate, TimeUnit.MILLISECONDS);
667 } catch (IOException | URISyntaxException | InterruptedException e) {
668 logger.info("handleCommand fails", e);
672 private boolean handleEqualizerCommands(String channelId, Command command, Connection connection, Device device)
673 throws URISyntaxException {
674 if (command instanceof RefreshType) {
675 this.lastKnownEqualizer = null;
677 if (command instanceof DecimalType) {
678 DecimalType value = (DecimalType) command;
679 if (this.lastKnownEqualizer == null) {
680 updateEqualizerState();
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();
688 if (channelId.equals(CHANNEL_EQUALIZER_MIDRANGE)) {
689 newEqualizerSetting.mid = value.intValue();
691 if (channelId.equals(CHANNEL_EQUALIZER_TREBLE)) {
692 newEqualizerSetting.treble = value.intValue();
695 connection.setEqualizer(device, newEqualizerSetting);
697 } catch (HttpException | IOException | ConnectionException | InterruptedException e) {
698 logger.debug("Update equalizer failed", e);
699 this.lastKnownEqualizer = null;
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;
712 connection.textToSpeech(device, text, volume, lastKnownVolume);
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) {
722 if (volume == null && textToSpeechVolume != 0) {
723 volume = textToSpeechVolume;
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.
728 connection.announcement(device, speak, bodyText, title, volume, lastKnownVolume);
731 private void stopCurrentNotification() {
732 ScheduledFuture<?> currentNotifcationUpdateTimer = this.currentNotifcationUpdateTimer;
733 if (currentNotifcationUpdateTimer != null) {
734 this.currentNotifcationUpdateTimer = null;
735 currentNotifcationUpdateTimer.cancel(true);
737 JsonNotificationResponse currentNotification = this.currentNotification;
738 if (currentNotification != null) {
739 this.currentNotification = null;
740 Connection currentConnection = this.findConnection();
741 if (currentConnection != null) {
743 currentConnection.stopNotification(currentNotification);
744 } catch (IOException | URISyntaxException | InterruptedException e) {
745 logger.warn("Stop notification failed", e);
751 private void updateNotificationTimerState() {
752 boolean stopCurrentNotification = true;
753 JsonNotificationResponse currentNotification = this.currentNotification;
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;
764 } catch (IOException | URISyntaxException | InterruptedException e) {
765 logger.warn("update notification state fails", e);
767 if (stopCurrentNotification) {
768 if (currentNotification != null) {
769 String type = currentNotification.type;
771 if (type.equals("Reminder")) {
772 updateState(CHANNEL_REMIND, new StringType(""));
773 updateRemind = false;
775 if (type.equals("Alarm")) {
776 updateState(CHANNEL_PLAY_ALARM_SOUND, new StringType(""));
781 stopCurrentNotification();
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) {
791 this.logger.debug("Handle updateState {}", this.getThing().getUID());
793 if (deviceNotificationState != null) {
794 notificationVolumeLevel = deviceNotificationState.volumeLevel;
796 if (ascendingAlarmModel != null) {
797 ascendingAlarm = ascendingAlarmModel.ascendingAlarmEnabled;
799 if (playlists != null) {
800 this.playLists = playlists;
802 if (alarmSounds != null) {
803 this.alarmSounds = alarmSounds;
805 if (musicProviders != null) {
806 this.musicProviders = musicProviders;
808 if (!setDeviceAndUpdateThingState(accountHandler, device, null)) {
809 this.logger.debug("Handle updateState {} aborted: Not online", this.getThing().getUID());
812 if (device == null) {
813 this.logger.debug("Handle updateState {} aborted: No device", this.getThing().getUID());
817 if (this.disableUpdate) {
818 this.logger.debug("Handle updateState {} aborted: Disabled", this.getThing().getUID());
821 Connection connection = this.findConnection();
822 if (connection == null) {
826 if (this.lastKnownEqualizer == null) {
827 updateEqualizerState();
830 PlayerInfo playerInfo = null;
831 Provider provider = null;
832 InfoText infoText = null;
833 MainArt mainArt = null;
834 String musicProviderId = null;
835 Progress progress = null;
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;
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();
853 if (musicProviderId.equals("AMAZON MUSIC")) {
854 musicProviderId = "AMAZON_MUSIC";
856 if (musicProviderId.equals("CLOUD_PLAYER")) {
857 musicProviderId = "AMAZON_MUSIC";
859 if (musicProviderId.startsWith("TUNEIN")) {
860 musicProviderId = "TUNEIN";
862 if (musicProviderId.startsWith("IHEARTRADIO")) {
863 musicProviderId = "I_HEART_RADIO";
865 if (musicProviderId.equals("APPLE") && musicProviderId.contains("MUSIC")) {
866 musicProviderId = "APPLE_MUSIC";
870 progress = playerInfo.progress;
873 } catch (HttpException e) {
874 if (e.getCode() != 400) {
875 logger.info("getPlayer fails", e);
877 } catch (IOException | URISyntaxException | InterruptedException e) {
878 logger.info("getPlayer fails", e);
881 isPlaying = (playerInfo != null && "PLAYING".equals(playerInfo.state));
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;
893 if (showTime != null && showTime && mediaProgress != null && mediaLength != null) {
894 mediaProgressMs = mediaProgress * 1000;
895 mediaLengthMs = mediaLength * 1000;
896 mediaStartMs = System.currentTimeMillis() - mediaProgressMs;
898 if (updateProgressJob == null) {
899 updateProgressJob = scheduler.scheduleWithFixedDelay(this::updateMediaProgress, 1000, 1000,
900 TimeUnit.MILLISECONDS);
911 updateMediaProgress(true);
914 JsonMediaState mediaState = null;
916 if ("AMAZON_MUSIC".equalsIgnoreCase(musicProviderId) || "TUNEIN".equalsIgnoreCase(musicProviderId)) {
917 mediaState = connection.getMediaState(device);
919 } catch (HttpException e) {
920 if (e.getCode() == 400) {
921 updateState(CHANNEL_RADIO_STATION_ID, new StringType(""));
923 logger.info("getMediaState fails", e);
925 } catch (IOException | URISyntaxException | InterruptedException e) {
926 logger.info("getMediaState fails", e);
929 // handle music provider id
930 if (provider != null && isPlaying) {
931 if (musicProviderId != null) {
932 this.musicProviderId = musicProviderId;
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;
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) {
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;
975 if (!bluetoothMAC.isEmpty()) {
976 lastKnownBluetoothMAC = bluetoothMAC;
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)) {
988 if (!"PLAYING".equals(mediaState.currentState)) {
995 // handle title, subtitle, imageUrl
997 String subTitle1 = "";
998 String subTitle2 = "";
999 String imageUrl = "";
1000 if (infoText != null) {
1001 if (infoText.title != null) {
1002 title = infoText.title;
1004 if (infoText.subText1 != null) {
1005 subTitle1 = infoText.subText1;
1008 if (infoText.subText2 != null) {
1009 subTitle2 = infoText.subText2;
1012 if (mainArt != null) {
1013 if (mainArt.url != null) {
1014 imageUrl = mainArt.url;
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) {
1023 if ((imageUrl == null || imageUrl.isEmpty()) && entry.imageURL != null) {
1024 imageUrl = entry.imageURL;
1026 if ((subTitle1 == null || subTitle1.isEmpty()) && entry.radioStationSlogan != null) {
1027 subTitle1 = entry.radioStationSlogan;
1029 if ((subTitle2 == null || subTitle2.isEmpty()) && entry.radioStationLocation != null) {
1030 subTitle2 = entry.radioStationLocation;
1038 String providerDisplayName = "";
1039 if (provider != null) {
1040 if (provider.providerDisplayName != null) {
1041 providerDisplayName = Objects.requireNonNullElse(provider.providerDisplayName, providerDisplayName);
1043 String providerName = provider.providerName;
1044 if (providerName != null && !providerName.isEmpty() && providerDisplayName.isEmpty()) {
1045 providerDisplayName = provider.providerName;
1050 Integer volume = null;
1051 if (!connection.isSequenceNodeQueueRunning()) {
1052 if (mediaState != null) {
1053 volume = mediaState.volume;
1055 if (playerInfo != null && volume == null) {
1056 Volume volumnInfo = playerInfo.volume;
1057 if (volumnInfo != null) {
1058 volume = volumnInfo.volume;
1061 if (volume != null && volume > 0) {
1062 lastKnownVolume = volume;
1064 if (volume == null) {
1065 volume = lastKnownVolume;
1069 if (updateRemind && currentNotifcationUpdateTimer == null) {
1070 updateRemind = false;
1071 updateState(CHANNEL_REMIND, new StringType(""));
1073 if (updateAlarm && currentNotifcationUpdateTimer == null) {
1074 updateAlarm = false;
1075 updateState(CHANNEL_PLAY_ALARM_SOUND, new StringType(""));
1077 if (updateRoutine) {
1078 updateRoutine = false;
1079 updateState(CHANNEL_START_ROUTINE, new StringType(""));
1081 if (updateTextToSpeech) {
1082 updateTextToSpeech = false;
1083 updateState(CHANNEL_TEXT_TO_SPEECH, new StringType(""));
1085 if (updatePlayMusicVoiceCommand) {
1086 updatePlayMusicVoiceCommand = false;
1087 updateState(CHANNEL_PLAY_MUSIC_VOICE_COMMAND, new StringType(""));
1089 if (updateStartCommand) {
1090 updateStartCommand = false;
1091 updateState(CHANNEL_START_COMMAND, new StringType(""));
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));
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));
1115 updateState(CHANNEL_ASCENDING_ALARM,
1116 ascendingAlarm != null ? (ascendingAlarm ? OnOffType.ON : OnOffType.OFF) : UnDefType.UNDEF);
1118 final Integer notificationVolumeLevel = this.notificationVolumeLevel;
1119 if (notificationVolumeLevel != null) {
1120 updateState(CHANNEL_NOTIFICATION_VOLUME, new PercentType(notificationVolumeLevel));
1122 updateState(CHANNEL_NOTIFICATION_VOLUME, UnDefType.UNDEF);
1124 } catch (Exception e) {
1125 this.logger.debug("Handle updateState {} failed: {}", this.getThing().getUID(), e.getMessage(), e);
1127 disableUpdate = false;
1128 throw e; // Rethrow same exception
1132 private void updateEqualizerState() {
1133 if (!this.capabilities.contains("SOUND_SETTINGS")) {
1137 Connection connection = findConnection();
1138 if (connection == null) {
1141 Device device = findDevice();
1142 if (device == null) {
1145 Integer bass = null;
1146 Integer midrange = null;
1147 Integer treble = null;
1149 JsonEqualizer equalizer = connection.getEqualizer(device);
1150 if (equalizer != null) {
1151 bass = equalizer.bass;
1152 midrange = equalizer.mid;
1153 treble = equalizer.treble;
1155 this.lastKnownEqualizer = equalizer;
1156 } catch (IOException | URISyntaxException | HttpException | ConnectionException | InterruptedException e) {
1157 logger.debug("Get equalizer failes", e);
1161 updateState(CHANNEL_EQUALIZER_BASS, new DecimalType(bass));
1163 if (midrange != null) {
1164 updateState(CHANNEL_EQUALIZER_MIDRANGE, new DecimalType(midrange));
1166 if (treble != null) {
1167 updateState(CHANNEL_EQUALIZER_TREBLE, new DecimalType(treble));
1171 private void updateMediaProgress() {
1172 updateMediaProgress(false);
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));
1184 updateState(CHANNEL_MEDIA_PROGRESS, UnDefType.UNDEF);
1186 updateState(CHANNEL_MEDIA_PROGRESS_TIME, new QuantityType<>(currentPlayTimeMs / 1000, Units.SECOND));
1187 if (updateMediaLength) {
1188 updateState(CHANNEL_MEDIA_LENGTH, new QuantityType<>(mediaLengthMs / 1000, Units.SECOND));
1191 updateState(CHANNEL_MEDIA_PROGRESS, UnDefType.UNDEF);
1192 updateState(CHANNEL_MEDIA_LENGTH, UnDefType.UNDEF);
1193 updateState(CHANNEL_MEDIA_PROGRESS_TIME, UnDefType.UNDEF);
1194 if (updateMediaLength) {
1195 updateState(CHANNEL_MEDIA_LENGTH, UnDefType.UNDEF);
1201 public void handlePushActivity(Activity pushActivity) {
1202 if ("DISCARDED_NON_DEVICE_DIRECTED_INTENT".equals(pushActivity.activityStatus)) {
1205 Description description = pushActivity.parseDescription();
1206 String firstUtteranceId = description.firstUtteranceId;
1207 if (firstUtteranceId == null || firstUtteranceId.isEmpty()
1208 || firstUtteranceId.toLowerCase().startsWith("textclient:")) {
1211 String firstStreamId = description.firstStreamId;
1212 if (firstStreamId == null || firstStreamId.isEmpty()) {
1215 String spokenText = description.summary;
1216 if (spokenText != null && !spokenText.isEmpty()) {
1218 String wakeWordPrefix = this.wakeWord;
1219 if (wakeWordPrefix != null) {
1220 wakeWordPrefix += " ";
1221 if (spokenText.toLowerCase().startsWith(wakeWordPrefix.toLowerCase())) {
1222 spokenText = spokenText.substring(wakeWordPrefix.length());
1226 if (lastSpokenText.isEmpty() || lastSpokenText.equals(spokenText)) {
1227 updateState(CHANNEL_LAST_VOICE_COMMAND, new StringType(""));
1229 lastSpokenText = spokenText;
1230 updateState(CHANNEL_LAST_VOICE_COMMAND, new StringType(spokenText));
1234 public void handlePushCommand(String command, String payload) {
1235 this.logger.debug("Handle push command {}", command);
1237 case "PUSH_VOLUME_CHANGE":
1238 JsonCommandPayloadPushVolumeChange volumeChange = Objects
1239 .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushVolumeChange.class));
1240 Connection connection = this.findConnection();
1241 Integer volumeSetting = volumeChange.volumeSetting;
1242 Boolean muted = volumeChange.isMuted;
1243 if (muted != null && muted) {
1244 updateState(CHANNEL_VOLUME, new PercentType(0));
1246 if (volumeSetting != null && connection != null && !connection.isSequenceNodeQueueRunning()) {
1247 lastKnownVolume = volumeSetting;
1248 updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume));
1251 case "PUSH_EQUALIZER_STATE_CHANGE":
1252 updateEqualizerState();
1255 AccountHandler account = this.account;
1256 Device device = this.device;
1257 if (account != null && device != null) {
1258 this.disableUpdate = false;
1259 updateState(account, device, null, null, null, null, null, null);
1264 public void updateNotifications(ZonedDateTime currentTime, ZonedDateTime now,
1265 @Nullable JsonCommandPayloadPushNotificationChange pushPayload, JsonNotificationResponse[] notifications) {
1266 Device device = this.device;
1267 if (device == null) {
1271 ZonedDateTime nextReminder = null;
1272 ZonedDateTime nextAlarm = null;
1273 ZonedDateTime nextMusicAlarm = null;
1274 ZonedDateTime nextTimer = null;
1275 for (JsonNotificationResponse notification : notifications) {
1276 if (Objects.equals(notification.deviceSerialNumber, device.serialNumber)) {
1277 // notification for this device
1278 if ("ON".equals(notification.status)) {
1279 if ("Reminder".equals(notification.type)) {
1280 String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1281 ZonedDateTime alarmTime = ZonedDateTime
1282 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1283 String recurringPattern = notification.recurringPattern;
1284 if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
1285 continue; // Ignore recurring entry if alarm time is before now
1287 if (nextReminder == null || alarmTime.isBefore(nextReminder)) {
1288 nextReminder = alarmTime;
1290 } else if ("Timer".equals(notification.type)) {
1291 // use remaining time
1292 ZonedDateTime alarmTime = currentTime.plus(notification.remainingTime, ChronoUnit.MILLIS);
1293 if (nextTimer == null || alarmTime.isBefore(nextTimer)) {
1294 nextTimer = alarmTime;
1296 } else if ("Alarm".equals(notification.type)) {
1297 String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1298 ZonedDateTime alarmTime = ZonedDateTime
1299 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1300 String recurringPattern = notification.recurringPattern;
1301 if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
1302 continue; // Ignore recurring entry if alarm time is before now
1304 if (nextAlarm == null || alarmTime.isBefore(nextAlarm)) {
1305 nextAlarm = alarmTime;
1307 } else if ("MusicAlarm".equals(notification.type)) {
1308 String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1309 ZonedDateTime alarmTime = ZonedDateTime
1310 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1311 String recurringPattern = notification.recurringPattern;
1312 if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
1313 continue; // Ignore recurring entry if alarm time is before now
1315 if (nextMusicAlarm == null || alarmTime.isBefore(nextMusicAlarm)) {
1316 nextMusicAlarm = alarmTime;
1323 updateState(CHANNEL_NEXT_REMINDER, nextReminder == null ? UnDefType.UNDEF : new DateTimeType(nextReminder));
1324 updateState(CHANNEL_NEXT_ALARM, nextAlarm == null ? UnDefType.UNDEF : new DateTimeType(nextAlarm));
1325 updateState(CHANNEL_NEXT_MUSIC_ALARM,
1326 nextMusicAlarm == null ? UnDefType.UNDEF : new DateTimeType(nextMusicAlarm));
1327 updateState(CHANNEL_NEXT_TIMER, nextTimer == null ? UnDefType.UNDEF : new DateTimeType(nextTimer));
1331 public void updateChannelState(String channelId, State state) {
1332 updateState(channelId, state);