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.format.DateTimeFormatter;
23 import java.time.temporal.ChronoUnit;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27 import java.util.stream.Collectors;
28 import java.util.stream.Stream;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.amazonechocontrol.internal.Connection;
33 import org.openhab.binding.amazonechocontrol.internal.ConnectionException;
34 import org.openhab.binding.amazonechocontrol.internal.HttpException;
35 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandler;
36 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandlerAnnouncement;
37 import org.openhab.binding.amazonechocontrol.internal.channelhandler.IEchoThingHandler;
38 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity;
39 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity.Description;
40 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm.AscendingAlarmModel;
41 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates;
42 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState;
43 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.PairedDevice;
44 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushNotificationChange;
45 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushVolumeChange;
46 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState.DeviceNotificationState;
47 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device;
48 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonEqualizer;
49 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState;
50 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState.QueueEntry;
51 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider;
52 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse;
53 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound;
54 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState;
55 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo;
56 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.InfoText;
57 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.MainArt;
58 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Progress;
59 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Provider;
60 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Volume;
61 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists;
62 import org.openhab.core.library.types.DateTimeType;
63 import org.openhab.core.library.types.DecimalType;
64 import org.openhab.core.library.types.IncreaseDecreaseType;
65 import org.openhab.core.library.types.NextPreviousType;
66 import org.openhab.core.library.types.OnOffType;
67 import org.openhab.core.library.types.PercentType;
68 import org.openhab.core.library.types.PlayPauseType;
69 import org.openhab.core.library.types.QuantityType;
70 import org.openhab.core.library.types.RewindFastforwardType;
71 import org.openhab.core.library.types.StringType;
72 import org.openhab.core.library.unit.Units;
73 import org.openhab.core.thing.Bridge;
74 import org.openhab.core.thing.ChannelUID;
75 import org.openhab.core.thing.Thing;
76 import org.openhab.core.thing.ThingStatus;
77 import org.openhab.core.thing.ThingUID;
78 import org.openhab.core.thing.binding.BaseThingHandler;
79 import org.openhab.core.types.Command;
80 import org.openhab.core.types.RefreshType;
81 import org.openhab.core.types.State;
82 import org.openhab.core.types.UnDefType;
83 import org.slf4j.Logger;
84 import org.slf4j.LoggerFactory;
86 import com.google.gson.Gson;
89 * The {@link EchoHandler} is responsible for the handling of the echo device
91 * @author Michael Geramb - Initial contribution
94 public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
95 private final Logger logger = LoggerFactory.getLogger(EchoHandler.class);
97 private @Nullable Device device;
98 private Set<String> capabilities = new HashSet<>();
99 private @Nullable AccountHandler account;
100 private @Nullable ScheduledFuture<?> updateStateJob;
101 private @Nullable ScheduledFuture<?> updateProgressJob;
102 private Object progressLock = new Object();
103 private @Nullable String wakeWord;
104 private @Nullable String lastKnownRadioStationId;
105 private @Nullable String lastKnownBluetoothMAC;
106 private @Nullable String lastKnownAmazonMusicId;
107 private String musicProviderId = "TUNEIN";
108 private boolean isPlaying = false;
109 private boolean isPaused = false;
110 private int lastKnownVolume = 25;
111 private int textToSpeechVolume = 0;
112 private @Nullable JsonEqualizer lastKnownEqualizer = null;
113 private @Nullable BluetoothState bluetoothState;
114 private boolean disableUpdate = false;
115 private boolean updateRemind = true;
116 private boolean updateTextToSpeech = true;
117 private boolean updateTextCommand = true;
118 private boolean updateAlarm = true;
119 private boolean updateRoutine = true;
120 private boolean updatePlayMusicVoiceCommand = true;
121 private boolean updateStartCommand = true;
122 private @Nullable Integer notificationVolumeLevel;
123 private @Nullable Boolean ascendingAlarm;
124 private @Nullable JsonPlaylists playLists;
125 private @Nullable JsonNotificationSound @Nullable [] alarmSounds;
126 private @Nullable List<JsonMusicProvider> musicProviders;
127 private List<ChannelHandler> channelHandlers = new ArrayList<>();
129 private @Nullable JsonNotificationResponse currentNotification;
130 private @Nullable ScheduledFuture<?> currentNotifcationUpdateTimer;
132 long mediaProgressMs;
134 String lastSpokenText = "";
136 public EchoHandler(Thing thing, Gson gson) {
139 channelHandlers.add(new ChannelHandlerAnnouncement(this, this.gson));
143 public void initialize() {
144 logger.debug("Amazon Echo Control Binding initialized");
145 Bridge bridge = this.getBridge();
146 if (bridge != null) {
147 AccountHandler account = (AccountHandler) bridge.getHandler();
148 if (account != null) {
149 setDeviceAndUpdateThingState(account, this.device, null);
150 account.addEchoHandler(this);
155 public boolean setDeviceAndUpdateThingState(AccountHandler accountHandler, @Nullable Device device,
156 @Nullable String wakeWord) {
157 this.account = accountHandler;
158 if (wakeWord != null) {
159 this.wakeWord = wakeWord;
161 if (device == null) {
162 updateStatus(ThingStatus.UNKNOWN);
165 this.device = device;
166 String[] capabilities = device.capabilities;
167 if (capabilities != null) {
168 this.capabilities = Stream.of(capabilities).filter(Objects::nonNull).collect(Collectors.toSet());
170 if (!device.online) {
171 updateStatus(ThingStatus.OFFLINE);
174 updateStatus(ThingStatus.ONLINE);
179 public void dispose() {
180 stopCurrentNotification();
181 ScheduledFuture<?> updateStateJob = this.updateStateJob;
182 this.updateStateJob = null;
183 if (updateStateJob != null) {
184 this.disableUpdate = false;
185 updateStateJob.cancel(false);
191 private void stopProgressTimer() {
192 ScheduledFuture<?> updateProgressJob = this.updateProgressJob;
193 this.updateProgressJob = null;
194 if (updateProgressJob != null) {
195 updateProgressJob.cancel(false);
199 public @Nullable BluetoothState findBluetoothState() {
200 return this.bluetoothState;
203 public @Nullable JsonPlaylists findPlaylists() {
204 return this.playLists;
207 public @Nullable JsonNotificationSound @Nullable [] findAlarmSounds() {
208 return this.alarmSounds;
211 public @Nullable List<JsonMusicProvider> findMusicProviders() {
212 return this.musicProviders;
215 private @Nullable Connection findConnection() {
216 AccountHandler accountHandler = this.account;
217 if (accountHandler != null) {
218 return accountHandler.findConnection();
223 public @Nullable AccountHandler findAccount() {
227 public @Nullable Device findDevice() {
231 public String findSerialNumber() {
232 String id = (String) getConfig().get(DEVICE_PROPERTY_SERIAL_NUMBER);
240 public void handleCommand(ChannelUID channelUID, Command command) {
242 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
243 int waitForUpdate = 1000;
244 boolean needBluetoothRefresh = false;
245 String lastKnownBluetoothMAC = this.lastKnownBluetoothMAC;
247 ScheduledFuture<?> updateStateJob = this.updateStateJob;
248 this.updateStateJob = null;
249 if (updateStateJob != null) {
250 this.disableUpdate = false;
251 updateStateJob.cancel(false);
253 AccountHandler account = this.account;
254 if (account == null) {
257 Connection connection = account.findConnection();
258 if (connection == null) {
261 Device device = this.device;
262 if (device == null) {
266 String channelId = channelUID.getId();
267 for (ChannelHandler channelHandler : channelHandlers) {
268 if (channelHandler.tryHandleCommand(device, connection, channelId, command)) {
274 if (channelId.equals(CHANNEL_PLAYER)) {
275 if (command == PlayPauseType.PAUSE || command == OnOffType.OFF) {
276 connection.command(device, "{\"type\":\"PauseCommand\"}");
277 } else if (command == PlayPauseType.PLAY || command == OnOffType.ON) {
279 connection.command(device, "{\"type\":\"PlayCommand\"}");
281 connection.playMusicVoiceCommand(device, this.musicProviderId, "!");
282 waitForUpdate = 3000;
284 } else if (command == NextPreviousType.NEXT) {
285 connection.command(device, "{\"type\":\"NextCommand\"}");
286 } else if (command == NextPreviousType.PREVIOUS) {
287 connection.command(device, "{\"type\":\"PreviousCommand\"}");
288 } else if (command == RewindFastforwardType.FASTFORWARD) {
289 connection.command(device, "{\"type\":\"ForwardCommand\"}");
290 } else if (command == RewindFastforwardType.REWIND) {
291 connection.command(device, "{\"type\":\"RewindCommand\"}");
294 // Notification commands
295 if (channelId.equals(CHANNEL_NOTIFICATION_VOLUME)) {
296 if (command instanceof PercentType) {
297 int volume = ((PercentType) command).intValue();
298 connection.notificationVolume(device, volume);
299 this.notificationVolumeLevel = volume;
301 account.forceCheckData();
304 if (channelId.equals(CHANNEL_ASCENDING_ALARM)) {
305 if (command == OnOffType.OFF) {
306 connection.ascendingAlarm(device, false);
307 this.ascendingAlarm = false;
309 account.forceCheckData();
311 if (command == OnOffType.ON) {
312 connection.ascendingAlarm(device, true);
313 this.ascendingAlarm = true;
315 account.forceCheckData();
318 // Media progress commands
319 Long mediaPosition = null;
320 if (channelId.equals(CHANNEL_MEDIA_PROGRESS)) {
321 if (command instanceof PercentType) {
322 PercentType value = (PercentType) command;
323 int percent = value.intValue();
324 mediaPosition = Math.round((mediaLengthMs / 1000d) * (percent / 100d));
327 if (channelId.equals(CHANNEL_MEDIA_PROGRESS_TIME)) {
328 if (command instanceof DecimalType) {
329 DecimalType value = (DecimalType) command;
330 mediaPosition = value.longValue();
332 if (command instanceof QuantityType<?>) {
333 QuantityType<?> value = (QuantityType<?>) command;
335 QuantityType<?> seconds = value.toUnit(Units.SECOND);
336 if (seconds != null) {
337 mediaPosition = seconds.longValue();
341 if (mediaPosition != null) {
343 synchronized (progressLock) {
344 String seekCommand = "{\"type\":\"SeekCommand\",\"mediaPosition\":" + mediaPosition
345 + ",\"contentFocusClientId\":null}";
346 connection.command(device, seekCommand);
347 connection.command(device, seekCommand); // Must be sent twice, the first one is ignored sometimes
348 this.mediaProgressMs = mediaPosition * 1000;
349 mediaStartMs = System.currentTimeMillis() - this.mediaProgressMs;
350 updateMediaProgress(false);
354 if (channelId.equals(CHANNEL_VOLUME)) {
355 Integer volume = null;
356 if (command instanceof PercentType) {
357 PercentType value = (PercentType) command;
358 volume = value.intValue();
359 } else if (command == OnOffType.OFF) {
361 } else if (command == OnOffType.ON) {
362 volume = lastKnownVolume;
363 } else if (command == IncreaseDecreaseType.INCREASE) {
364 if (lastKnownVolume < 100) {
366 volume = lastKnownVolume;
368 } else if (command == IncreaseDecreaseType.DECREASE) {
369 if (lastKnownVolume > 0) {
371 volume = lastKnownVolume;
374 if (volume != null) {
375 if ("WHA".equals(device.deviceFamily)) {
376 connection.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + volume
377 + ",\"contentFocusClientId\":\"Default\"}");
379 connection.volume(device, volume);
381 lastKnownVolume = volume;
382 updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume));
386 // equalizer commands
387 if (channelId.equals(CHANNEL_EQUALIZER_BASS) || channelId.equals(CHANNEL_EQUALIZER_MIDRANGE)
388 || channelId.equals(CHANNEL_EQUALIZER_TREBLE)) {
389 if (handleEqualizerCommands(channelId, command, connection, device)) {
395 if (channelId.equals(CHANNEL_SHUFFLE)) {
396 if (command instanceof OnOffType) {
397 OnOffType value = (OnOffType) command;
399 connection.command(device, "{\"type\":\"ShuffleCommand\",\"shuffle\":\""
400 + (value == OnOffType.ON ? "true" : "false") + "\"}");
404 // play music command
405 if (channelId.equals(CHANNEL_MUSIC_PROVIDER_ID)) {
406 if (command instanceof StringType) {
408 String musicProviderId = command.toFullString();
409 if (!musicProviderId.equals(this.musicProviderId)) {
410 this.musicProviderId = musicProviderId;
411 if (this.isPlaying) {
412 connection.playMusicVoiceCommand(device, this.musicProviderId, "!");
413 waitForUpdate = 3000;
418 if (channelId.equals(CHANNEL_PLAY_MUSIC_VOICE_COMMAND)) {
419 if (command instanceof StringType) {
420 String voiceCommand = command.toFullString();
421 if (!this.musicProviderId.isEmpty()) {
422 connection.playMusicVoiceCommand(device, this.musicProviderId, voiceCommand);
423 waitForUpdate = 3000;
424 updatePlayMusicVoiceCommand = true;
429 // bluetooth commands
430 if (channelId.equals(CHANNEL_BLUETOOTH_MAC)) {
431 needBluetoothRefresh = true;
432 if (command instanceof StringType) {
433 String address = ((StringType) command).toFullString();
434 if (!address.isEmpty()) {
435 waitForUpdate = 4000;
437 connection.bluetooth(device, address);
440 if (channelId.equals(CHANNEL_BLUETOOTH)) {
441 needBluetoothRefresh = true;
442 if (command == OnOffType.ON) {
443 waitForUpdate = 4000;
444 String bluetoothId = lastKnownBluetoothMAC;
445 BluetoothState state = bluetoothState;
446 if (state != null && (bluetoothId == null || bluetoothId.isEmpty())) {
447 PairedDevice[] pairedDeviceList = state.pairedDeviceList;
448 if (pairedDeviceList != null) {
449 for (PairedDevice paired : pairedDeviceList) {
450 if (paired == null) {
453 String pairedAddress = paired.address;
454 if (pairedAddress != null && !pairedAddress.isEmpty()) {
455 lastKnownBluetoothMAC = pairedAddress;
461 if (lastKnownBluetoothMAC != null && !lastKnownBluetoothMAC.isEmpty()) {
462 connection.bluetooth(device, lastKnownBluetoothMAC);
464 } else if (command == OnOffType.OFF) {
465 connection.bluetooth(device, null);
468 if (channelId.equals(CHANNEL_BLUETOOTH_DEVICE_NAME)) {
469 needBluetoothRefresh = true;
471 // amazon music commands
472 if (channelId.equals(CHANNEL_AMAZON_MUSIC_TRACK_ID)) {
473 if (command instanceof StringType) {
474 String trackId = command.toFullString();
475 if (!trackId.isEmpty()) {
476 waitForUpdate = 3000;
478 connection.playAmazonMusicTrack(device, trackId);
481 if (channelId.equals(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID)) {
482 if (command instanceof StringType) {
483 String playListId = command.toFullString();
484 if (!playListId.isEmpty()) {
485 waitForUpdate = 3000;
487 connection.playAmazonMusicPlayList(device, playListId);
490 if (channelId.equals(CHANNEL_AMAZON_MUSIC)) {
491 if (command == OnOffType.ON) {
492 String lastKnownAmazonMusicId = this.lastKnownAmazonMusicId;
493 if (lastKnownAmazonMusicId != null && !lastKnownAmazonMusicId.isEmpty()) {
494 waitForUpdate = 3000;
496 connection.playAmazonMusicTrack(device, lastKnownAmazonMusicId);
497 } else if (command == OnOffType.OFF) {
498 connection.playAmazonMusicTrack(device, "");
503 if (channelId.equals(CHANNEL_RADIO_STATION_ID)) {
504 if (command instanceof StringType) {
505 String stationId = command.toFullString();
506 if (!stationId.isEmpty()) {
507 waitForUpdate = 3000;
509 connection.playRadio(device, stationId);
512 if (channelId.equals(CHANNEL_RADIO)) {
513 if (command == OnOffType.ON) {
514 String lastKnownRadioStationId = this.lastKnownRadioStationId;
515 if (lastKnownRadioStationId != null && !lastKnownRadioStationId.isEmpty()) {
516 waitForUpdate = 3000;
518 connection.playRadio(device, lastKnownRadioStationId);
519 } else if (command == OnOffType.OFF) {
520 connection.playRadio(device, "");
525 if (channelId.equals(CHANNEL_REMIND)) {
526 if (command instanceof StringType) {
527 stopCurrentNotification();
528 String reminder = command.toFullString();
529 if (!reminder.isEmpty()) {
530 waitForUpdate = 3000;
532 currentNotification = connection.notification(device, "Reminder", reminder, null);
533 currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> {
534 updateNotificationTimerState();
535 }, 1, 1, TimeUnit.SECONDS);
539 if (channelId.equals(CHANNEL_PLAY_ALARM_SOUND)) {
540 if (command instanceof StringType) {
541 stopCurrentNotification();
542 String alarmSound = command.toFullString();
543 if (!alarmSound.isEmpty()) {
544 waitForUpdate = 3000;
546 String[] parts = alarmSound.split(":", 2);
547 JsonNotificationSound sound = new JsonNotificationSound();
548 if (parts.length == 2) {
549 sound.providerId = parts[0];
552 sound.providerId = "ECHO";
553 sound.id = alarmSound;
555 currentNotification = connection.notification(device, "Alarm", null, sound);
556 currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> {
557 updateNotificationTimerState();
558 }, 1, 1, TimeUnit.SECONDS);
564 if (channelId.equals(CHANNEL_TEXT_TO_SPEECH)) {
565 if (command instanceof StringType) {
566 String text = command.toFullString();
567 if (!text.isEmpty()) {
568 waitForUpdate = 1000;
569 updateTextToSpeech = true;
570 startTextToSpeech(connection, device, text);
574 if (channelId.equals(CHANNEL_TEXT_TO_SPEECH_VOLUME)) {
575 if (command instanceof PercentType) {
576 PercentType value = (PercentType) command;
577 textToSpeechVolume = value.intValue();
578 } else if (command == OnOffType.OFF) {
579 textToSpeechVolume = 0;
580 } else if (command == OnOffType.ON) {
581 textToSpeechVolume = lastKnownVolume;
582 } else if (command == IncreaseDecreaseType.INCREASE) {
583 if (textToSpeechVolume < 100) {
584 textToSpeechVolume++;
586 } else if (command == IncreaseDecreaseType.DECREASE) {
587 if (textToSpeechVolume > 0) {
588 textToSpeechVolume--;
591 this.updateState(channelId, new PercentType(textToSpeechVolume));
593 if (channelId.equals(CHANNEL_TEXT_COMMAND)) {
594 if (command instanceof StringType) {
595 String text = command.toFullString();
596 if (!text.isEmpty()) {
597 waitForUpdate = 1000;
598 updateTextCommand = true;
599 startTextCommand(connection, device, text);
603 if (channelId.equals(CHANNEL_LAST_VOICE_COMMAND)) {
604 if (command instanceof StringType) {
605 String text = command.toFullString();
606 if (!text.isEmpty()) {
608 startTextToSpeech(connection, device, text);
612 if (channelId.equals(CHANNEL_START_COMMAND)) {
613 if (command instanceof StringType) {
614 String commandText = command.toFullString();
615 if (!commandText.isEmpty()) {
616 updateStartCommand = true;
617 if (commandText.startsWith(FLASH_BRIEFING_COMMAND_PREFIX)) {
618 // Handle custom flashbriefings commands
619 String flashBriefingId = commandText.substring(FLASH_BRIEFING_COMMAND_PREFIX.length());
620 for (FlashBriefingProfileHandler flashBriefingHandler : account
621 .getFlashBriefingProfileHandlers()) {
622 ThingUID flashBriefingUid = flashBriefingHandler.getThing().getUID();
623 if (flashBriefingId.equals(flashBriefingHandler.getThing().getUID().getId())) {
624 flashBriefingHandler.handleCommand(
625 new ChannelUID(flashBriefingUid, CHANNEL_PLAY_ON_DEVICE),
626 new StringType(device.serialNumber));
631 // Handle standard commands
632 if (!commandText.startsWith("Alexa.")) {
633 commandText = "Alexa." + commandText + ".Play";
635 waitForUpdate = 1000;
636 connection.executeSequenceCommand(device, commandText, Map.of());
641 if (channelId.equals(CHANNEL_START_ROUTINE)) {
642 if (command instanceof StringType) {
643 String utterance = command.toFullString();
644 if (!utterance.isEmpty()) {
645 waitForUpdate = 1000;
646 updateRoutine = true;
647 connection.startRoutine(device, utterance);
651 if (waitForUpdate < 0) {
654 // force update of the state
655 this.disableUpdate = true;
656 final boolean bluetoothRefresh = needBluetoothRefresh;
657 Runnable doRefresh = () -> {
658 this.disableUpdate = false;
659 BluetoothState state = null;
660 if (bluetoothRefresh) {
661 JsonBluetoothStates states;
662 states = connection.getBluetoothConnectionStates();
663 if (states != null) {
664 state = states.findStateByDevice(device);
668 updateState(account, device, state, null, null, null, null, null);
670 if (command instanceof RefreshType) {
672 account.forceCheckData();
674 if (waitForUpdate == 0) {
677 this.updateStateJob = scheduler.schedule(doRefresh, waitForUpdate, TimeUnit.MILLISECONDS);
679 } catch (IOException | URISyntaxException | InterruptedException e) {
680 logger.info("handleCommand fails", e);
684 private boolean handleEqualizerCommands(String channelId, Command command, Connection connection, Device device)
685 throws URISyntaxException {
686 if (command instanceof RefreshType) {
687 this.lastKnownEqualizer = null;
689 if (command instanceof DecimalType) {
690 DecimalType value = (DecimalType) command;
691 if (this.lastKnownEqualizer == null) {
692 updateEqualizerState();
694 JsonEqualizer lastKnownEqualizer = this.lastKnownEqualizer;
695 if (lastKnownEqualizer != null) {
696 JsonEqualizer newEqualizerSetting = lastKnownEqualizer.createClone();
697 if (channelId.equals(CHANNEL_EQUALIZER_BASS)) {
698 newEqualizerSetting.bass = value.intValue();
700 if (channelId.equals(CHANNEL_EQUALIZER_MIDRANGE)) {
701 newEqualizerSetting.mid = value.intValue();
703 if (channelId.equals(CHANNEL_EQUALIZER_TREBLE)) {
704 newEqualizerSetting.treble = value.intValue();
707 connection.setEqualizer(device, newEqualizerSetting);
709 } catch (HttpException | IOException | ConnectionException | InterruptedException e) {
710 logger.debug("Update equalizer failed", e);
711 this.lastKnownEqualizer = null;
718 private void startTextToSpeech(Connection connection, Device device, String text)
719 throws IOException, URISyntaxException {
720 Integer volume = null;
721 if (textToSpeechVolume != 0) {
722 volume = textToSpeechVolume;
724 connection.textToSpeech(device, text, volume, lastKnownVolume);
727 private void startTextCommand(Connection connection, Device device, String text)
728 throws IOException, URISyntaxException {
729 Integer volume = null;
730 if (textToSpeechVolume != 0) {
731 volume = textToSpeechVolume;
733 connection.textCommand(device, text, volume, lastKnownVolume);
737 public void startAnnouncement(Device device, String speak, String bodyText, @Nullable String title,
738 @Nullable Integer volume) throws IOException, URISyntaxException {
739 Connection connection = this.findConnection();
740 if (connection == null) {
743 if (volume == null && textToSpeechVolume != 0) {
744 volume = textToSpeechVolume;
746 if (volume != null && volume < 0) {
747 volume = null; // the meaning of negative values is 'do not use'. The api requires null in this case.
749 connection.announcement(device, speak, bodyText, title, volume, lastKnownVolume);
752 private void stopCurrentNotification() {
753 ScheduledFuture<?> currentNotifcationUpdateTimer = this.currentNotifcationUpdateTimer;
754 if (currentNotifcationUpdateTimer != null) {
755 this.currentNotifcationUpdateTimer = null;
756 currentNotifcationUpdateTimer.cancel(true);
758 JsonNotificationResponse currentNotification = this.currentNotification;
759 if (currentNotification != null) {
760 this.currentNotification = null;
761 Connection currentConnection = this.findConnection();
762 if (currentConnection != null) {
764 currentConnection.stopNotification(currentNotification);
765 } catch (IOException | URISyntaxException | InterruptedException e) {
766 logger.warn("Stop notification failed", e);
772 private void updateNotificationTimerState() {
773 boolean stopCurrentNotification = true;
774 JsonNotificationResponse currentNotification = this.currentNotification;
776 if (currentNotification != null) {
777 Connection currentConnection = this.findConnection();
778 if (currentConnection != null) {
779 JsonNotificationResponse newState = currentConnection.getNotificationState(currentNotification);
780 if (newState != null && "ON".equals(newState.status)) {
781 stopCurrentNotification = false;
785 } catch (IOException | URISyntaxException | InterruptedException e) {
786 logger.warn("update notification state fails", e);
788 if (stopCurrentNotification) {
789 if (currentNotification != null) {
790 String type = currentNotification.type;
792 if (type.equals("Reminder")) {
793 updateState(CHANNEL_REMIND, StringType.EMPTY);
794 updateRemind = false;
796 if (type.equals("Alarm")) {
797 updateState(CHANNEL_PLAY_ALARM_SOUND, StringType.EMPTY);
802 stopCurrentNotification();
806 public void updateState(AccountHandler accountHandler, @Nullable Device device,
807 @Nullable BluetoothState bluetoothState, @Nullable DeviceNotificationState deviceNotificationState,
808 @Nullable AscendingAlarmModel ascendingAlarmModel, @Nullable JsonPlaylists playlists,
809 @Nullable JsonNotificationSound @Nullable [] alarmSounds,
810 @Nullable List<JsonMusicProvider> musicProviders) {
812 this.logger.debug("Handle updateState {}", this.getThing().getUID());
814 if (deviceNotificationState != null) {
815 notificationVolumeLevel = deviceNotificationState.volumeLevel;
817 if (ascendingAlarmModel != null) {
818 ascendingAlarm = ascendingAlarmModel.ascendingAlarmEnabled;
820 if (playlists != null) {
821 this.playLists = playlists;
823 if (alarmSounds != null) {
824 this.alarmSounds = alarmSounds;
826 if (musicProviders != null) {
827 this.musicProviders = musicProviders;
829 if (!setDeviceAndUpdateThingState(accountHandler, device, null)) {
830 this.logger.debug("Handle updateState {} aborted: Not online", this.getThing().getUID());
833 if (device == null) {
834 this.logger.debug("Handle updateState {} aborted: No device", this.getThing().getUID());
838 if (this.disableUpdate) {
839 this.logger.debug("Handle updateState {} aborted: Disabled", this.getThing().getUID());
842 Connection connection = this.findConnection();
843 if (connection == null) {
847 if (this.lastKnownEqualizer == null) {
848 updateEqualizerState();
851 PlayerInfo playerInfo = null;
852 Provider provider = null;
853 InfoText infoText = null;
854 MainArt mainArt = null;
855 String musicProviderId = null;
856 Progress progress = null;
858 JsonPlayerState playerState = connection.getPlayer(device);
859 if (playerState != null) {
860 playerInfo = playerState.playerInfo;
861 if (playerInfo != null) {
862 infoText = playerInfo.infoText;
863 if (infoText == null) {
864 infoText = playerInfo.miniInfoText;
866 mainArt = playerInfo.mainArt;
867 provider = playerInfo.provider;
868 if (provider != null) {
869 musicProviderId = provider.providerName;
870 // Map the music provider id to the one used for starting music with voice command
871 if (musicProviderId != null) {
872 musicProviderId = musicProviderId.toUpperCase();
874 if (musicProviderId.equals("AMAZON MUSIC")) {
875 musicProviderId = "AMAZON_MUSIC";
877 if (musicProviderId.equals("CLOUD_PLAYER")) {
878 musicProviderId = "AMAZON_MUSIC";
880 if (musicProviderId.startsWith("TUNEIN")) {
881 musicProviderId = "TUNEIN";
883 if (musicProviderId.startsWith("IHEARTRADIO")) {
884 musicProviderId = "I_HEART_RADIO";
886 if (musicProviderId.equals("APPLE") && musicProviderId.contains("MUSIC")) {
887 musicProviderId = "APPLE_MUSIC";
891 progress = playerInfo.progress;
894 } catch (HttpException e) {
895 if (e.getCode() != 400) {
896 logger.info("getPlayer fails", e);
898 } catch (IOException | URISyntaxException | InterruptedException e) {
899 logger.info("getPlayer fails", e);
902 isPlaying = (playerInfo != null && "PLAYING".equals(playerInfo.state));
904 isPaused = (playerInfo != null && "PAUSED".equals(playerInfo.state));
905 synchronized (progressLock) {
906 Boolean showTime = null;
907 Long mediaLength = null;
908 Long mediaProgress = null;
909 if (progress != null) {
910 showTime = progress.showTiming;
911 mediaLength = progress.mediaLength;
912 mediaProgress = progress.mediaProgress;
914 if (showTime != null && showTime && mediaProgress != null && mediaLength != null) {
915 mediaProgressMs = mediaProgress * 1000;
916 mediaLengthMs = mediaLength * 1000;
917 mediaStartMs = System.currentTimeMillis() - mediaProgressMs;
919 if (updateProgressJob == null) {
920 updateProgressJob = scheduler.scheduleWithFixedDelay(this::updateMediaProgress, 1000, 1000,
921 TimeUnit.MILLISECONDS);
932 updateMediaProgress(true);
935 JsonMediaState mediaState = null;
937 if ("AMAZON_MUSIC".equalsIgnoreCase(musicProviderId) || "TUNEIN".equalsIgnoreCase(musicProviderId)) {
938 mediaState = connection.getMediaState(device);
940 } catch (HttpException e) {
941 if (e.getCode() == 400) {
942 updateState(CHANNEL_RADIO_STATION_ID, StringType.EMPTY);
944 logger.info("getMediaState fails", e);
946 } catch (IOException | URISyntaxException | InterruptedException e) {
947 logger.info("getMediaState fails", e);
950 // handle music provider id
951 if (provider != null && isPlaying) {
952 if (musicProviderId != null) {
953 this.musicProviderId = musicProviderId;
957 // handle amazon music
958 String amazonMusicTrackId = "";
959 String amazonMusicPlayListId = "";
960 boolean amazonMusic = false;
961 if (mediaState != null) {
962 String contentId = mediaState.contentId;
963 if (isPlaying && "CLOUD_PLAYER".equals(mediaState.providerId) && contentId != null
964 && !contentId.isEmpty()) {
965 amazonMusicTrackId = contentId;
966 lastKnownAmazonMusicId = amazonMusicTrackId;
972 String bluetoothMAC = "";
973 String bluetoothDeviceName = "";
974 boolean bluetoothIsConnected = false;
975 if (bluetoothState != null) {
976 this.bluetoothState = bluetoothState;
977 PairedDevice[] pairedDeviceList = bluetoothState.pairedDeviceList;
978 if (pairedDeviceList != null) {
979 for (PairedDevice paired : pairedDeviceList) {
980 if (paired == null) {
983 String pairedAddress = paired.address;
984 if (paired.connected && pairedAddress != null) {
985 bluetoothIsConnected = true;
986 bluetoothMAC = pairedAddress;
987 bluetoothDeviceName = paired.friendlyName;
988 if (bluetoothDeviceName == null || bluetoothDeviceName.isEmpty()) {
989 bluetoothDeviceName = pairedAddress;
996 if (!bluetoothMAC.isEmpty()) {
997 lastKnownBluetoothMAC = bluetoothMAC;
1001 boolean isRadio = false;
1002 String radioStationId = "";
1003 if (mediaState != null) {
1004 radioStationId = Objects.requireNonNullElse(mediaState.radioStationId, "");
1005 if (!radioStationId.isEmpty()) {
1006 lastKnownRadioStationId = radioStationId;
1007 if ("TUNEIN".equalsIgnoreCase(musicProviderId)) {
1009 if (!"PLAYING".equals(mediaState.currentState)) {
1010 radioStationId = "";
1016 // handle title, subtitle, imageUrl
1018 String subTitle1 = "";
1019 String subTitle2 = "";
1020 String imageUrl = "";
1021 if (infoText != null) {
1022 if (infoText.title != null) {
1023 title = infoText.title;
1025 if (infoText.subText1 != null) {
1026 subTitle1 = infoText.subText1;
1029 if (infoText.subText2 != null) {
1030 subTitle2 = infoText.subText2;
1033 if (mainArt != null) {
1034 if (mainArt.url != null) {
1035 imageUrl = mainArt.url;
1038 if (mediaState != null) {
1039 QueueEntry[] queueEntries = mediaState.queue;
1040 if (queueEntries != null && queueEntries.length > 0) {
1041 QueueEntry entry = queueEntries[0];
1042 if (entry != null) {
1044 if ((imageUrl == null || imageUrl.isEmpty()) && entry.imageURL != null) {
1045 imageUrl = entry.imageURL;
1047 if ((subTitle1 == null || subTitle1.isEmpty()) && entry.radioStationSlogan != null) {
1048 subTitle1 = entry.radioStationSlogan;
1050 if ((subTitle2 == null || subTitle2.isEmpty()) && entry.radioStationLocation != null) {
1051 subTitle2 = entry.radioStationLocation;
1059 String providerDisplayName = "";
1060 if (provider != null) {
1061 if (provider.providerDisplayName != null) {
1062 providerDisplayName = Objects.requireNonNullElse(provider.providerDisplayName, providerDisplayName);
1064 String providerName = provider.providerName;
1065 if (providerName != null && !providerName.isEmpty() && providerDisplayName.isEmpty()) {
1066 providerDisplayName = provider.providerName;
1071 Integer volume = null;
1072 if (!connection.isSequenceNodeQueueRunning()) {
1073 if (mediaState != null) {
1074 volume = mediaState.volume;
1076 if (playerInfo != null && volume == null) {
1077 Volume volumnInfo = playerInfo.volume;
1078 if (volumnInfo != null) {
1079 volume = volumnInfo.volume;
1082 if (volume != null && volume > 0) {
1083 lastKnownVolume = volume;
1085 if (volume == null) {
1086 volume = lastKnownVolume;
1090 if (updateRemind && currentNotifcationUpdateTimer == null) {
1091 updateRemind = false;
1092 updateState(CHANNEL_REMIND, StringType.EMPTY);
1094 if (updateAlarm && currentNotifcationUpdateTimer == null) {
1095 updateAlarm = false;
1096 updateState(CHANNEL_PLAY_ALARM_SOUND, StringType.EMPTY);
1098 if (updateRoutine) {
1099 updateRoutine = false;
1100 updateState(CHANNEL_START_ROUTINE, StringType.EMPTY);
1102 if (updateTextToSpeech) {
1103 updateTextToSpeech = false;
1104 updateState(CHANNEL_TEXT_TO_SPEECH, StringType.EMPTY);
1106 if (updateTextCommand) {
1107 updateTextCommand = false;
1108 updateState(CHANNEL_TEXT_COMMAND, StringType.EMPTY);
1110 if (updatePlayMusicVoiceCommand) {
1111 updatePlayMusicVoiceCommand = false;
1112 updateState(CHANNEL_PLAY_MUSIC_VOICE_COMMAND, StringType.EMPTY);
1114 if (updateStartCommand) {
1115 updateStartCommand = false;
1116 updateState(CHANNEL_START_COMMAND, StringType.EMPTY);
1119 updateState(CHANNEL_MUSIC_PROVIDER_ID, new StringType(musicProviderId));
1120 updateState(CHANNEL_AMAZON_MUSIC_TRACK_ID, new StringType(amazonMusicTrackId));
1121 updateState(CHANNEL_AMAZON_MUSIC, isPlaying && amazonMusic ? OnOffType.ON : OnOffType.OFF);
1122 updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID, new StringType(amazonMusicPlayListId));
1123 updateState(CHANNEL_RADIO_STATION_ID, new StringType(radioStationId));
1124 updateState(CHANNEL_RADIO, isPlaying && isRadio ? OnOffType.ON : OnOffType.OFF);
1125 updateState(CHANNEL_PROVIDER_DISPLAY_NAME, new StringType(providerDisplayName));
1126 updateState(CHANNEL_PLAYER, isPlaying ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
1127 updateState(CHANNEL_IMAGE_URL, new StringType(imageUrl));
1128 updateState(CHANNEL_TITLE, new StringType(title));
1129 if (volume != null) {
1130 updateState(CHANNEL_VOLUME, new PercentType(volume));
1132 updateState(CHANNEL_SUBTITLE1, new StringType(subTitle1));
1133 updateState(CHANNEL_SUBTITLE2, new StringType(subTitle2));
1134 if (bluetoothState != null) {
1135 updateState(CHANNEL_BLUETOOTH, bluetoothIsConnected ? OnOffType.ON : OnOffType.OFF);
1136 updateState(CHANNEL_BLUETOOTH_MAC, new StringType(bluetoothMAC));
1137 updateState(CHANNEL_BLUETOOTH_DEVICE_NAME, new StringType(bluetoothDeviceName));
1140 updateState(CHANNEL_ASCENDING_ALARM,
1141 ascendingAlarm != null ? (ascendingAlarm ? OnOffType.ON : OnOffType.OFF) : UnDefType.UNDEF);
1143 final Integer notificationVolumeLevel = this.notificationVolumeLevel;
1144 if (notificationVolumeLevel != null) {
1145 updateState(CHANNEL_NOTIFICATION_VOLUME, new PercentType(notificationVolumeLevel));
1147 updateState(CHANNEL_NOTIFICATION_VOLUME, UnDefType.UNDEF);
1149 } catch (Exception e) {
1150 this.logger.debug("Handle updateState {} failed: {}", this.getThing().getUID(), e.getMessage(), e);
1152 disableUpdate = false;
1153 throw e; // Rethrow same exception
1157 private void updateEqualizerState() {
1158 if (!this.capabilities.contains("SOUND_SETTINGS")) {
1162 Connection connection = findConnection();
1163 if (connection == null) {
1166 Device device = findDevice();
1167 if (device == null) {
1170 Integer bass = null;
1171 Integer midrange = null;
1172 Integer treble = null;
1174 JsonEqualizer equalizer = connection.getEqualizer(device);
1175 if (equalizer != null) {
1176 bass = equalizer.bass;
1177 midrange = equalizer.mid;
1178 treble = equalizer.treble;
1180 this.lastKnownEqualizer = equalizer;
1181 } catch (IOException | URISyntaxException | HttpException | ConnectionException | InterruptedException e) {
1182 logger.debug("Get equalizer failes", e);
1186 updateState(CHANNEL_EQUALIZER_BASS, new DecimalType(bass));
1188 if (midrange != null) {
1189 updateState(CHANNEL_EQUALIZER_MIDRANGE, new DecimalType(midrange));
1191 if (treble != null) {
1192 updateState(CHANNEL_EQUALIZER_TREBLE, new DecimalType(treble));
1196 private void updateMediaProgress() {
1197 updateMediaProgress(false);
1200 private void updateMediaProgress(boolean updateMediaLength) {
1201 synchronized (progressLock) {
1202 if (mediaStartMs > 0) {
1203 long currentPlayTimeMs = isPlaying ? System.currentTimeMillis() - mediaStartMs : mediaProgressMs;
1204 if (mediaLengthMs > 0) {
1205 int progressPercent = (int) Math.min(100,
1206 Math.round((double) currentPlayTimeMs / (double) mediaLengthMs * 100));
1207 updateState(CHANNEL_MEDIA_PROGRESS, new PercentType(progressPercent));
1209 updateState(CHANNEL_MEDIA_PROGRESS, UnDefType.UNDEF);
1211 updateState(CHANNEL_MEDIA_PROGRESS_TIME, new QuantityType<>(currentPlayTimeMs / 1000, Units.SECOND));
1212 if (updateMediaLength) {
1213 updateState(CHANNEL_MEDIA_LENGTH, new QuantityType<>(mediaLengthMs / 1000, Units.SECOND));
1216 updateState(CHANNEL_MEDIA_PROGRESS, UnDefType.UNDEF);
1217 updateState(CHANNEL_MEDIA_LENGTH, UnDefType.UNDEF);
1218 updateState(CHANNEL_MEDIA_PROGRESS_TIME, UnDefType.UNDEF);
1219 if (updateMediaLength) {
1220 updateState(CHANNEL_MEDIA_LENGTH, UnDefType.UNDEF);
1226 public void handlePushActivity(Activity pushActivity) {
1227 if ("DISCARDED_NON_DEVICE_DIRECTED_INTENT".equals(pushActivity.activityStatus)) {
1230 Description description = pushActivity.parseDescription();
1231 String firstUtteranceId = description.firstUtteranceId;
1232 if (firstUtteranceId == null || firstUtteranceId.isEmpty()
1233 || firstUtteranceId.toLowerCase().startsWith("textclient:")) {
1236 String firstStreamId = description.firstStreamId;
1237 if (firstStreamId == null || firstStreamId.isEmpty()) {
1240 String spokenText = description.summary;
1241 if (spokenText != null && !spokenText.isEmpty()) {
1243 String wakeWordPrefix = this.wakeWord;
1244 if (wakeWordPrefix != null) {
1245 wakeWordPrefix += " ";
1246 if (spokenText.toLowerCase().startsWith(wakeWordPrefix.toLowerCase())) {
1247 spokenText = spokenText.substring(wakeWordPrefix.length());
1251 if (lastSpokenText.isEmpty() || lastSpokenText.equals(spokenText)) {
1252 updateState(CHANNEL_LAST_VOICE_COMMAND, StringType.EMPTY);
1254 lastSpokenText = spokenText;
1255 updateState(CHANNEL_LAST_VOICE_COMMAND, new StringType(spokenText));
1259 public void handlePushCommand(String command, String payload) {
1260 this.logger.debug("Handle push command {}", command);
1262 case "PUSH_VOLUME_CHANGE":
1263 JsonCommandPayloadPushVolumeChange volumeChange = Objects
1264 .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushVolumeChange.class));
1265 Connection connection = this.findConnection();
1266 Integer volumeSetting = volumeChange.volumeSetting;
1267 Boolean muted = volumeChange.isMuted;
1268 if (muted != null && muted) {
1269 updateState(CHANNEL_VOLUME, new PercentType(0));
1271 if (volumeSetting != null && connection != null && !connection.isSequenceNodeQueueRunning()) {
1272 lastKnownVolume = volumeSetting;
1273 updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume));
1276 case "PUSH_EQUALIZER_STATE_CHANGE":
1277 updateEqualizerState();
1280 AccountHandler account = this.account;
1281 Device device = this.device;
1282 if (account != null && device != null) {
1283 this.disableUpdate = false;
1284 updateState(account, device, null, null, null, null, null, null);
1289 public void updateNotifications(ZonedDateTime currentTime, ZonedDateTime now,
1290 @Nullable JsonCommandPayloadPushNotificationChange pushPayload, JsonNotificationResponse[] notifications) {
1291 Device device = this.device;
1292 if (device == null) {
1296 ZonedDateTime nextReminder = null;
1297 ZonedDateTime nextAlarm = null;
1298 ZonedDateTime nextMusicAlarm = null;
1299 ZonedDateTime nextTimer = null;
1300 for (JsonNotificationResponse notification : notifications) {
1301 if (Objects.equals(notification.deviceSerialNumber, device.serialNumber)) {
1302 // notification for this device
1303 if ("ON".equals(notification.status)) {
1304 if ("Reminder".equals(notification.type)) {
1305 String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1306 String date = notification.originalDate != null ? notification.originalDate
1307 : ZonedDateTime.now().toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
1308 String time = notification.originalTime != null ? notification.originalTime : "00:00:00";
1309 ZonedDateTime alarmTime = ZonedDateTime.parse(date + "T" + time + offset,
1310 DateTimeFormatter.ISO_DATE_TIME);
1311 String recurringPattern = notification.recurringPattern;
1312 if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
1313 continue; // Ignore recurring entry if alarm time is before now
1315 if (nextReminder == null || alarmTime.isBefore(nextReminder)) {
1316 nextReminder = alarmTime;
1318 } else if ("Timer".equals(notification.type)) {
1319 // use remaining time
1320 ZonedDateTime alarmTime = currentTime.plus(notification.remainingTime, ChronoUnit.MILLIS);
1321 if (nextTimer == null || alarmTime.isBefore(nextTimer)) {
1322 nextTimer = alarmTime;
1324 } else if ("Alarm".equals(notification.type)) {
1325 String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1326 ZonedDateTime alarmTime = ZonedDateTime
1327 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1328 String recurringPattern = notification.recurringPattern;
1329 if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
1330 continue; // Ignore recurring entry if alarm time is before now
1332 if (nextAlarm == null || alarmTime.isBefore(nextAlarm)) {
1333 nextAlarm = alarmTime;
1335 } else if ("MusicAlarm".equals(notification.type)) {
1336 String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1337 ZonedDateTime alarmTime = ZonedDateTime
1338 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1339 String recurringPattern = notification.recurringPattern;
1340 if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
1341 continue; // Ignore recurring entry if alarm time is before now
1343 if (nextMusicAlarm == null || alarmTime.isBefore(nextMusicAlarm)) {
1344 nextMusicAlarm = alarmTime;
1351 updateState(CHANNEL_NEXT_REMINDER, nextReminder == null ? UnDefType.UNDEF : new DateTimeType(nextReminder));
1352 updateState(CHANNEL_NEXT_ALARM, nextAlarm == null ? UnDefType.UNDEF : new DateTimeType(nextAlarm));
1353 updateState(CHANNEL_NEXT_MUSIC_ALARM,
1354 nextMusicAlarm == null ? UnDefType.UNDEF : new DateTimeType(nextMusicAlarm));
1355 updateState(CHANNEL_NEXT_TIMER, nextTimer == null ? UnDefType.UNDEF : new DateTimeType(nextTimer));
1359 public void updateChannelState(String channelId, State state) {
1360 updateState(channelId, state);