2 * Copyright (c) 2010-2023 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;
24 import java.util.ArrayList;
25 import java.util.HashSet;
26 import java.util.List;
28 import java.util.Objects;
30 import java.util.concurrent.ScheduledFuture;
31 import java.util.concurrent.TimeUnit;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.amazonechocontrol.internal.Connection;
36 import org.openhab.binding.amazonechocontrol.internal.ConnectionException;
37 import org.openhab.binding.amazonechocontrol.internal.HttpException;
38 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandler;
39 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandlerAnnouncement;
40 import org.openhab.binding.amazonechocontrol.internal.channelhandler.IEchoThingHandler;
41 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity;
42 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity.Description;
43 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm.AscendingAlarmModel;
44 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates;
45 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState;
46 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.PairedDevice;
47 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushNotificationChange;
48 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushVolumeChange;
49 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState.DeviceNotificationState;
50 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device;
51 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonEqualizer;
52 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState;
53 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState.QueueEntry;
54 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider;
55 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse;
56 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound;
57 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState;
58 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo;
59 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.InfoText;
60 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.MainArt;
61 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Progress;
62 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Provider;
63 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Volume;
64 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists;
65 import org.openhab.core.library.types.DateTimeType;
66 import org.openhab.core.library.types.DecimalType;
67 import org.openhab.core.library.types.IncreaseDecreaseType;
68 import org.openhab.core.library.types.NextPreviousType;
69 import org.openhab.core.library.types.OnOffType;
70 import org.openhab.core.library.types.PercentType;
71 import org.openhab.core.library.types.PlayPauseType;
72 import org.openhab.core.library.types.QuantityType;
73 import org.openhab.core.library.types.RewindFastforwardType;
74 import org.openhab.core.library.types.StringType;
75 import org.openhab.core.library.unit.Units;
76 import org.openhab.core.thing.Bridge;
77 import org.openhab.core.thing.ChannelUID;
78 import org.openhab.core.thing.Thing;
79 import org.openhab.core.thing.ThingStatus;
80 import org.openhab.core.thing.ThingUID;
81 import org.openhab.core.thing.binding.BaseThingHandler;
82 import org.openhab.core.types.Command;
83 import org.openhab.core.types.RefreshType;
84 import org.openhab.core.types.State;
85 import org.openhab.core.types.UnDefType;
86 import org.slf4j.Logger;
87 import org.slf4j.LoggerFactory;
89 import com.google.gson.Gson;
92 * The {@link EchoHandler} is responsible for the handling of the echo device
94 * @author Michael Geramb - Initial contribution
97 public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
98 private final Logger logger = LoggerFactory.getLogger(EchoHandler.class);
100 private @Nullable Device device;
101 private Set<String> capabilities = new HashSet<>();
102 private @Nullable AccountHandler account;
103 private @Nullable ScheduledFuture<?> updateStateJob;
104 private @Nullable ScheduledFuture<?> updateProgressJob;
105 private Object progressLock = new Object();
106 private @Nullable String wakeWord;
107 private @Nullable String lastKnownRadioStationId;
108 private @Nullable String lastKnownBluetoothMAC;
109 private @Nullable String lastKnownAmazonMusicId;
110 private String musicProviderId = "TUNEIN";
111 private boolean isPlaying = false;
112 private boolean isPaused = false;
113 private int lastKnownVolume = 25;
114 private int textToSpeechVolume = 0;
115 private @Nullable JsonEqualizer lastKnownEqualizer = null;
116 private @Nullable BluetoothState bluetoothState;
117 private boolean disableUpdate = false;
118 private boolean updateRemind = true;
119 private boolean updateTextToSpeech = true;
120 private boolean updateTextCommand = true;
121 private boolean updateAlarm = true;
122 private boolean updateRoutine = true;
123 private boolean updatePlayMusicVoiceCommand = true;
124 private boolean updateStartCommand = true;
125 private @Nullable Integer notificationVolumeLevel;
126 private @Nullable Boolean ascendingAlarm;
127 private @Nullable JsonPlaylists playLists;
128 private List<JsonNotificationSound> alarmSounds = List.of();
129 private List<JsonMusicProvider> musicProviders = List.of();
130 private List<ChannelHandler> channelHandlers = new ArrayList<>();
132 private @Nullable JsonNotificationResponse currentNotification;
133 private @Nullable ScheduledFuture<?> currentNotifcationUpdateTimer;
135 long mediaProgressMs;
137 String lastSpokenText = "";
139 public EchoHandler(Thing thing, Gson gson) {
142 channelHandlers.add(new ChannelHandlerAnnouncement(this, this.gson));
146 public void initialize() {
147 logger.debug("Amazon Echo Control Binding initialized");
148 Bridge bridge = this.getBridge();
149 if (bridge != null) {
150 AccountHandler account = (AccountHandler) bridge.getHandler();
151 if (account != null) {
152 setDeviceAndUpdateThingState(account, this.device, null);
153 account.addEchoHandler(this);
158 public boolean setDeviceAndUpdateThingState(AccountHandler accountHandler, @Nullable Device device,
159 @Nullable String wakeWord) {
160 this.account = accountHandler;
161 if (wakeWord != null) {
162 this.wakeWord = wakeWord;
164 if (device == null) {
165 updateStatus(ThingStatus.UNKNOWN);
168 this.device = device;
169 this.capabilities = device.getCapabilities();
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 List<JsonNotificationSound> findAlarmSounds() {
208 return this.alarmSounds;
211 public 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 percentCommand) {
297 int volume = percentCommand.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 percentCommand) {
322 int percent = percentCommand.intValue();
323 mediaPosition = Math.round((mediaLengthMs / 1000d) * (percent / 100d));
326 if (channelId.equals(CHANNEL_MEDIA_PROGRESS_TIME)) {
327 if (command instanceof DecimalType decimalCommand) {
328 mediaPosition = decimalCommand.longValue();
330 if (command instanceof QuantityType<?> quantityCommand) {
332 QuantityType<?> seconds = quantityCommand.toUnit(Units.SECOND);
333 if (seconds != null) {
334 mediaPosition = seconds.longValue();
338 if (mediaPosition != null) {
340 synchronized (progressLock) {
341 String seekCommand = "{\"type\":\"SeekCommand\",\"mediaPosition\":" + mediaPosition
342 + ",\"contentFocusClientId\":null}";
343 connection.command(device, seekCommand);
344 connection.command(device, seekCommand); // Must be sent twice, the first one is ignored sometimes
345 this.mediaProgressMs = mediaPosition * 1000;
346 mediaStartMs = System.currentTimeMillis() - this.mediaProgressMs;
347 updateMediaProgress(false);
351 if (channelId.equals(CHANNEL_VOLUME)) {
352 Integer volume = null;
353 if (command instanceof PercentType percentCommand) {
354 volume = percentCommand.intValue();
355 } else if (command == OnOffType.OFF) {
357 } else if (command == OnOffType.ON) {
358 volume = lastKnownVolume;
359 } else if (command == IncreaseDecreaseType.INCREASE) {
360 if (lastKnownVolume < 100) {
362 volume = lastKnownVolume;
364 } else if (command == IncreaseDecreaseType.DECREASE) {
365 if (lastKnownVolume > 0) {
367 volume = lastKnownVolume;
370 if (volume != null) {
371 if ("WHA".equals(device.deviceFamily)) {
372 connection.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + volume
373 + ",\"contentFocusClientId\":\"Default\"}");
375 connection.volume(device, volume);
377 lastKnownVolume = volume;
378 updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume));
382 // equalizer commands
383 if (channelId.equals(CHANNEL_EQUALIZER_BASS) || channelId.equals(CHANNEL_EQUALIZER_MIDRANGE)
384 || channelId.equals(CHANNEL_EQUALIZER_TREBLE)) {
385 if (handleEqualizerCommands(channelId, command, connection, device)) {
391 if (channelId.equals(CHANNEL_SHUFFLE)) {
392 if (command instanceof OnOffType value) {
394 connection.command(device, "{\"type\":\"ShuffleCommand\",\"shuffle\":\""
395 + (value == OnOffType.ON ? "true" : "false") + "\"}");
399 // play music command
400 if (channelId.equals(CHANNEL_MUSIC_PROVIDER_ID)) {
401 if (command instanceof StringType) {
403 String musicProviderId = command.toFullString();
404 if (!musicProviderId.equals(this.musicProviderId)) {
405 this.musicProviderId = musicProviderId;
406 if (this.isPlaying) {
407 connection.playMusicVoiceCommand(device, this.musicProviderId, "!");
408 waitForUpdate = 3000;
413 if (channelId.equals(CHANNEL_PLAY_MUSIC_VOICE_COMMAND)) {
414 if (command instanceof StringType) {
415 String voiceCommand = command.toFullString();
416 if (!this.musicProviderId.isEmpty()) {
417 connection.playMusicVoiceCommand(device, this.musicProviderId, voiceCommand);
418 waitForUpdate = 3000;
419 updatePlayMusicVoiceCommand = true;
424 // bluetooth commands
425 if (channelId.equals(CHANNEL_BLUETOOTH_MAC)) {
426 needBluetoothRefresh = true;
427 if (command instanceof StringType stringCommand) {
428 String address = stringCommand.toFullString();
429 if (!address.isEmpty()) {
430 waitForUpdate = 4000;
432 connection.bluetooth(device, address);
435 if (channelId.equals(CHANNEL_BLUETOOTH)) {
436 needBluetoothRefresh = true;
437 if (command == OnOffType.ON) {
438 waitForUpdate = 4000;
439 String bluetoothId = lastKnownBluetoothMAC;
440 BluetoothState state = bluetoothState;
441 if (state != null && (bluetoothId == null || bluetoothId.isEmpty())) {
442 for (PairedDevice paired : state.getPairedDeviceList()) {
443 String pairedAddress = paired.address;
444 if (pairedAddress != null && !pairedAddress.isEmpty()) {
445 lastKnownBluetoothMAC = pairedAddress;
450 if (lastKnownBluetoothMAC != null && !lastKnownBluetoothMAC.isEmpty()) {
451 connection.bluetooth(device, lastKnownBluetoothMAC);
453 } else if (command == OnOffType.OFF) {
454 connection.bluetooth(device, null);
457 if (channelId.equals(CHANNEL_BLUETOOTH_DEVICE_NAME)) {
458 needBluetoothRefresh = true;
460 // amazon music commands
461 if (channelId.equals(CHANNEL_AMAZON_MUSIC_TRACK_ID)) {
462 if (command instanceof StringType) {
463 String trackId = command.toFullString();
464 if (!trackId.isEmpty()) {
465 waitForUpdate = 3000;
467 connection.playAmazonMusicTrack(device, trackId);
470 if (channelId.equals(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID)) {
471 if (command instanceof StringType) {
472 String playListId = command.toFullString();
473 if (!playListId.isEmpty()) {
474 waitForUpdate = 3000;
476 connection.playAmazonMusicPlayList(device, playListId);
479 if (channelId.equals(CHANNEL_AMAZON_MUSIC)) {
480 if (command == OnOffType.ON) {
481 String lastKnownAmazonMusicId = this.lastKnownAmazonMusicId;
482 if (lastKnownAmazonMusicId != null && !lastKnownAmazonMusicId.isEmpty()) {
483 waitForUpdate = 3000;
485 connection.playAmazonMusicTrack(device, lastKnownAmazonMusicId);
486 } else if (command == OnOffType.OFF) {
487 connection.playAmazonMusicTrack(device, "");
492 if (channelId.equals(CHANNEL_RADIO_STATION_ID)) {
493 if (command instanceof StringType) {
494 String stationId = command.toFullString();
495 if (!stationId.isEmpty()) {
496 waitForUpdate = 3000;
498 connection.playRadio(device, stationId);
501 if (channelId.equals(CHANNEL_RADIO)) {
502 if (command == OnOffType.ON) {
503 String lastKnownRadioStationId = this.lastKnownRadioStationId;
504 if (lastKnownRadioStationId != null && !lastKnownRadioStationId.isEmpty()) {
505 waitForUpdate = 3000;
507 connection.playRadio(device, lastKnownRadioStationId);
508 } else if (command == OnOffType.OFF) {
509 connection.playRadio(device, "");
514 if (channelId.equals(CHANNEL_REMIND)) {
515 if (command instanceof StringType) {
516 stopCurrentNotification();
517 String reminder = command.toFullString();
518 if (!reminder.isEmpty()) {
519 waitForUpdate = 3000;
521 currentNotification = connection.notification(device, "Reminder", reminder, null);
522 currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> {
523 updateNotificationTimerState();
524 }, 1, 1, TimeUnit.SECONDS);
528 if (channelId.equals(CHANNEL_PLAY_ALARM_SOUND)) {
529 if (command instanceof StringType) {
530 stopCurrentNotification();
531 String alarmSound = command.toFullString();
532 if (!alarmSound.isEmpty()) {
533 waitForUpdate = 3000;
535 String[] parts = alarmSound.split(":", 2);
536 JsonNotificationSound sound = new JsonNotificationSound();
537 if (parts.length == 2) {
538 sound.providerId = parts[0];
541 sound.providerId = "ECHO";
542 sound.id = alarmSound;
544 currentNotification = connection.notification(device, "Alarm", null, sound);
545 currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> {
546 updateNotificationTimerState();
547 }, 1, 1, TimeUnit.SECONDS);
553 if (channelId.equals(CHANNEL_TEXT_TO_SPEECH)) {
554 if (command instanceof StringType) {
555 String text = command.toFullString();
556 if (!text.isEmpty()) {
557 waitForUpdate = 1000;
558 updateTextToSpeech = true;
559 startTextToSpeech(connection, device, text);
563 if (channelId.equals(CHANNEL_TEXT_TO_SPEECH_VOLUME)) {
564 if (command instanceof PercentType percentCommand) {
565 textToSpeechVolume = percentCommand.intValue();
566 } else if (command == OnOffType.OFF) {
567 textToSpeechVolume = 0;
568 } else if (command == OnOffType.ON) {
569 textToSpeechVolume = lastKnownVolume;
570 } else if (command == IncreaseDecreaseType.INCREASE) {
571 if (textToSpeechVolume < 100) {
572 textToSpeechVolume++;
574 } else if (command == IncreaseDecreaseType.DECREASE) {
575 if (textToSpeechVolume > 0) {
576 textToSpeechVolume--;
579 this.updateState(channelId, new PercentType(textToSpeechVolume));
581 if (channelId.equals(CHANNEL_TEXT_COMMAND)) {
582 if (command instanceof StringType) {
583 String text = command.toFullString();
584 if (!text.isEmpty()) {
585 waitForUpdate = 1000;
586 updateTextCommand = true;
587 startTextCommand(connection, device, text);
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 decimalCommand) {
678 if (this.lastKnownEqualizer == null) {
679 updateEqualizerState();
681 JsonEqualizer lastKnownEqualizer = this.lastKnownEqualizer;
682 if (lastKnownEqualizer != null) {
683 JsonEqualizer newEqualizerSetting = lastKnownEqualizer.createClone();
684 if (channelId.equals(CHANNEL_EQUALIZER_BASS)) {
685 newEqualizerSetting.bass = decimalCommand.intValue();
687 if (channelId.equals(CHANNEL_EQUALIZER_MIDRANGE)) {
688 newEqualizerSetting.mid = decimalCommand.intValue();
690 if (channelId.equals(CHANNEL_EQUALIZER_TREBLE)) {
691 newEqualizerSetting.treble = decimalCommand.intValue();
694 connection.setEqualizer(device, newEqualizerSetting);
696 } catch (HttpException | IOException | ConnectionException | InterruptedException e) {
697 logger.debug("Update equalizer failed", e);
698 this.lastKnownEqualizer = null;
705 private void startTextToSpeech(Connection connection, Device device, String text)
706 throws IOException, URISyntaxException {
707 Integer volume = null;
708 if (textToSpeechVolume != 0) {
709 volume = textToSpeechVolume;
711 connection.textToSpeech(device, text, volume, lastKnownVolume);
714 private void startTextCommand(Connection connection, Device device, String text)
715 throws IOException, URISyntaxException {
716 Integer volume = null;
717 if (textToSpeechVolume != 0) {
718 volume = textToSpeechVolume;
720 connection.textCommand(device, text, volume, lastKnownVolume);
724 public void startAnnouncement(Device device, String speak, String bodyText, @Nullable String title,
725 @Nullable Integer volume) throws IOException, URISyntaxException {
726 Connection connection = this.findConnection();
727 if (connection == null) {
730 if (volume == null && textToSpeechVolume != 0) {
731 volume = textToSpeechVolume;
733 if (volume != null && volume < 0) {
734 volume = null; // the meaning of negative values is 'do not use'. The api requires null in this case.
736 connection.announcement(device, speak, bodyText, title, volume, lastKnownVolume);
739 private void stopCurrentNotification() {
740 ScheduledFuture<?> currentNotifcationUpdateTimer = this.currentNotifcationUpdateTimer;
741 if (currentNotifcationUpdateTimer != null) {
742 this.currentNotifcationUpdateTimer = null;
743 currentNotifcationUpdateTimer.cancel(true);
745 JsonNotificationResponse currentNotification = this.currentNotification;
746 if (currentNotification != null) {
747 this.currentNotification = null;
748 Connection currentConnection = this.findConnection();
749 if (currentConnection != null) {
751 currentConnection.stopNotification(currentNotification);
752 } catch (IOException | URISyntaxException | InterruptedException e) {
753 logger.warn("Stop notification failed", e);
759 private void updateNotificationTimerState() {
760 boolean stopCurrentNotification = true;
761 JsonNotificationResponse currentNotification = this.currentNotification;
763 if (currentNotification != null) {
764 Connection currentConnection = this.findConnection();
765 if (currentConnection != null) {
766 JsonNotificationResponse newState = currentConnection.getNotificationState(currentNotification);
767 if (newState != null && "ON".equals(newState.status)) {
768 stopCurrentNotification = false;
772 } catch (IOException | URISyntaxException | InterruptedException e) {
773 logger.warn("update notification state fails", e);
775 if (stopCurrentNotification) {
776 if (currentNotification != null) {
777 String type = currentNotification.type;
779 if ("Reminder".equals(type)) {
780 updateState(CHANNEL_REMIND, StringType.EMPTY);
781 updateRemind = false;
783 if ("Alarm".equals(type)) {
784 updateState(CHANNEL_PLAY_ALARM_SOUND, StringType.EMPTY);
789 stopCurrentNotification();
793 public void updateState(AccountHandler accountHandler, @Nullable Device device,
794 @Nullable BluetoothState bluetoothState, @Nullable DeviceNotificationState deviceNotificationState,
795 @Nullable AscendingAlarmModel ascendingAlarmModel, @Nullable JsonPlaylists playlists,
796 @Nullable List<JsonNotificationSound> alarmSounds, @Nullable List<JsonMusicProvider> musicProviders) {
798 this.logger.debug("Handle updateState {}", this.getThing().getUID());
800 if (deviceNotificationState != null) {
801 notificationVolumeLevel = deviceNotificationState.volumeLevel;
803 if (ascendingAlarmModel != null) {
804 ascendingAlarm = ascendingAlarmModel.ascendingAlarmEnabled;
806 if (playlists != null) {
807 this.playLists = playlists;
809 if (alarmSounds != null) {
810 this.alarmSounds = alarmSounds;
812 if (musicProviders != null) {
813 this.musicProviders = musicProviders;
815 if (!setDeviceAndUpdateThingState(accountHandler, device, null)) {
816 this.logger.debug("Handle updateState {} aborted: Not online", this.getThing().getUID());
819 if (device == null) {
820 this.logger.debug("Handle updateState {} aborted: No device", this.getThing().getUID());
824 if (this.disableUpdate) {
825 this.logger.debug("Handle updateState {} aborted: Disabled", this.getThing().getUID());
828 Connection connection = this.findConnection();
829 if (connection == null) {
833 if (this.lastKnownEqualizer == null) {
834 updateEqualizerState();
837 PlayerInfo playerInfo = null;
838 Provider provider = null;
839 InfoText infoText = null;
840 MainArt mainArt = null;
841 String musicProviderId = null;
842 Progress progress = null;
844 JsonPlayerState playerState = connection.getPlayer(device);
845 if (playerState != null) {
846 playerInfo = playerState.playerInfo;
847 if (playerInfo != null) {
848 infoText = playerInfo.infoText;
849 if (infoText == null) {
850 infoText = playerInfo.miniInfoText;
852 mainArt = playerInfo.mainArt;
853 provider = playerInfo.provider;
854 if (provider != null) {
855 musicProviderId = provider.providerName;
856 // Map the music provider id to the one used for starting music with voice command
857 if (musicProviderId != null) {
858 musicProviderId = musicProviderId.toUpperCase();
860 if ("AMAZON MUSIC".equals(musicProviderId)) {
861 musicProviderId = "AMAZON_MUSIC";
863 if ("CLOUD_PLAYER".equals(musicProviderId)) {
864 musicProviderId = "AMAZON_MUSIC";
866 if (musicProviderId.startsWith("TUNEIN")) {
867 musicProviderId = "TUNEIN";
869 if (musicProviderId.startsWith("IHEARTRADIO")) {
870 musicProviderId = "I_HEART_RADIO";
872 if ("APPLE".equals(musicProviderId) && musicProviderId.contains("MUSIC")) {
873 musicProviderId = "APPLE_MUSIC";
877 progress = playerInfo.progress;
880 } catch (HttpException e) {
881 if (e.getCode() != 400) {
882 logger.info("getPlayer fails", e);
884 } catch (IOException | URISyntaxException | InterruptedException e) {
885 logger.info("getPlayer fails", e);
888 isPlaying = (playerInfo != null && "PLAYING".equals(playerInfo.state));
890 isPaused = (playerInfo != null && "PAUSED".equals(playerInfo.state));
891 synchronized (progressLock) {
892 Boolean showTime = null;
893 Long mediaLength = null;
894 Long mediaProgress = null;
895 if (progress != null) {
896 showTime = progress.showTiming;
897 mediaLength = progress.mediaLength;
898 mediaProgress = progress.mediaProgress;
900 if (showTime != null && showTime && mediaProgress != null && mediaLength != null) {
901 mediaProgressMs = mediaProgress * 1000;
902 mediaLengthMs = mediaLength * 1000;
903 mediaStartMs = System.currentTimeMillis() - mediaProgressMs;
905 if (updateProgressJob == null) {
906 updateProgressJob = scheduler.scheduleWithFixedDelay(this::updateMediaProgress, 1000, 1000,
907 TimeUnit.MILLISECONDS);
918 updateMediaProgress(true);
921 JsonMediaState mediaState = null;
923 if ("AMAZON_MUSIC".equalsIgnoreCase(musicProviderId) || "TUNEIN".equalsIgnoreCase(musicProviderId)) {
924 mediaState = connection.getMediaState(device);
926 } catch (HttpException e) {
927 if (e.getCode() == 400) {
928 updateState(CHANNEL_RADIO_STATION_ID, StringType.EMPTY);
930 logger.info("getMediaState fails", e);
932 } catch (IOException | URISyntaxException | InterruptedException e) {
933 logger.info("getMediaState fails", e);
936 // handle music provider id
937 if (provider != null && isPlaying) {
938 if (musicProviderId != null) {
939 this.musicProviderId = musicProviderId;
943 // handle amazon music
944 String amazonMusicTrackId = "";
945 String amazonMusicPlayListId = "";
946 boolean amazonMusic = false;
947 if (mediaState != null) {
948 String contentId = mediaState.contentId;
949 if (isPlaying && "CLOUD_PLAYER".equals(mediaState.providerId) && contentId != null
950 && !contentId.isEmpty()) {
951 amazonMusicTrackId = contentId;
952 lastKnownAmazonMusicId = amazonMusicTrackId;
958 String bluetoothMAC = "";
959 String bluetoothDeviceName = "";
960 boolean bluetoothIsConnected = false;
961 if (bluetoothState != null) {
962 this.bluetoothState = bluetoothState;
963 for (PairedDevice paired : bluetoothState.getPairedDeviceList()) {
964 String pairedAddress = paired.address;
965 if (paired.connected && pairedAddress != null) {
966 bluetoothIsConnected = true;
967 bluetoothMAC = pairedAddress;
968 bluetoothDeviceName = paired.friendlyName;
969 if (bluetoothDeviceName == null || bluetoothDeviceName.isEmpty()) {
970 bluetoothDeviceName = pairedAddress;
977 if (!bluetoothMAC.isEmpty()) {
978 lastKnownBluetoothMAC = bluetoothMAC;
982 boolean isRadio = false;
983 String radioStationId = "";
984 if (mediaState != null) {
985 radioStationId = Objects.requireNonNullElse(mediaState.radioStationId, "");
986 if (!radioStationId.isEmpty()) {
987 lastKnownRadioStationId = radioStationId;
988 if ("TUNEIN".equalsIgnoreCase(musicProviderId)) {
990 if (!"PLAYING".equals(mediaState.currentState)) {
997 // handle title, subtitle, imageUrl
999 String subTitle1 = "";
1000 String subTitle2 = "";
1001 String imageUrl = "";
1002 if (infoText != null) {
1003 if (infoText.title != null) {
1004 title = infoText.title;
1006 if (infoText.subText1 != null) {
1007 subTitle1 = infoText.subText1;
1010 if (infoText.subText2 != null) {
1011 subTitle2 = infoText.subText2;
1014 if (mainArt != null) {
1015 if (mainArt.url != null) {
1016 imageUrl = mainArt.url;
1019 if (mediaState != null) {
1020 List<QueueEntry> queueEntries = Objects.requireNonNullElse(mediaState.queue, List.of());
1021 if (!queueEntries.isEmpty()) {
1022 QueueEntry entry = queueEntries.get(0);
1024 if ((imageUrl == null || imageUrl.isEmpty()) && entry.imageURL != null) {
1025 imageUrl = entry.imageURL;
1027 if ((subTitle1 == null || subTitle1.isEmpty()) && entry.radioStationSlogan != null) {
1028 subTitle1 = entry.radioStationSlogan;
1030 if ((subTitle2 == null || subTitle2.isEmpty()) && entry.radioStationLocation != null) {
1031 subTitle2 = entry.radioStationLocation;
1039 String providerDisplayName = "";
1040 if (provider != null) {
1041 if (provider.providerDisplayName != null) {
1042 providerDisplayName = Objects.requireNonNullElse(provider.providerDisplayName, providerDisplayName);
1044 String providerName = provider.providerName;
1045 if (providerName != null && !providerName.isEmpty() && providerDisplayName.isEmpty()) {
1046 providerDisplayName = provider.providerName;
1051 Integer volume = null;
1052 if (!connection.isSequenceNodeQueueRunning()) {
1053 if (mediaState != null) {
1054 volume = mediaState.volume;
1056 if (playerInfo != null && volume == null) {
1057 Volume volumnInfo = playerInfo.volume;
1058 if (volumnInfo != null) {
1059 volume = volumnInfo.volume;
1062 if (volume != null && volume > 0) {
1063 lastKnownVolume = volume;
1065 if (volume == null) {
1066 volume = lastKnownVolume;
1070 if (updateRemind && currentNotifcationUpdateTimer == null) {
1071 updateRemind = false;
1072 updateState(CHANNEL_REMIND, StringType.EMPTY);
1074 if (updateAlarm && currentNotifcationUpdateTimer == null) {
1075 updateAlarm = false;
1076 updateState(CHANNEL_PLAY_ALARM_SOUND, StringType.EMPTY);
1078 if (updateRoutine) {
1079 updateRoutine = false;
1080 updateState(CHANNEL_START_ROUTINE, StringType.EMPTY);
1082 if (updateTextToSpeech) {
1083 updateTextToSpeech = false;
1084 updateState(CHANNEL_TEXT_TO_SPEECH, StringType.EMPTY);
1086 if (updateTextCommand) {
1087 updateTextCommand = false;
1088 updateState(CHANNEL_TEXT_COMMAND, StringType.EMPTY);
1090 if (updatePlayMusicVoiceCommand) {
1091 updatePlayMusicVoiceCommand = false;
1092 updateState(CHANNEL_PLAY_MUSIC_VOICE_COMMAND, StringType.EMPTY);
1094 if (updateStartCommand) {
1095 updateStartCommand = false;
1096 updateState(CHANNEL_START_COMMAND, StringType.EMPTY);
1099 updateState(CHANNEL_MUSIC_PROVIDER_ID, new StringType(musicProviderId));
1100 updateState(CHANNEL_AMAZON_MUSIC_TRACK_ID, new StringType(amazonMusicTrackId));
1101 updateState(CHANNEL_AMAZON_MUSIC, isPlaying && amazonMusic ? OnOffType.ON : OnOffType.OFF);
1102 updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID, new StringType(amazonMusicPlayListId));
1103 updateState(CHANNEL_RADIO_STATION_ID, new StringType(radioStationId));
1104 updateState(CHANNEL_RADIO, isPlaying && isRadio ? OnOffType.ON : OnOffType.OFF);
1105 updateState(CHANNEL_PROVIDER_DISPLAY_NAME, new StringType(providerDisplayName));
1106 updateState(CHANNEL_PLAYER, isPlaying ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
1107 updateState(CHANNEL_IMAGE_URL, new StringType(imageUrl));
1108 updateState(CHANNEL_TITLE, new StringType(title));
1109 if (volume != null) {
1110 updateState(CHANNEL_VOLUME, new PercentType(volume));
1112 updateState(CHANNEL_SUBTITLE1, new StringType(subTitle1));
1113 updateState(CHANNEL_SUBTITLE2, new StringType(subTitle2));
1114 if (bluetoothState != null) {
1115 updateState(CHANNEL_BLUETOOTH, bluetoothIsConnected ? OnOffType.ON : OnOffType.OFF);
1116 updateState(CHANNEL_BLUETOOTH_MAC, new StringType(bluetoothMAC));
1117 updateState(CHANNEL_BLUETOOTH_DEVICE_NAME, new StringType(bluetoothDeviceName));
1120 updateState(CHANNEL_ASCENDING_ALARM,
1121 ascendingAlarm != null ? (ascendingAlarm ? OnOffType.ON : OnOffType.OFF) : UnDefType.UNDEF);
1123 final Integer notificationVolumeLevel = this.notificationVolumeLevel;
1124 if (notificationVolumeLevel != null) {
1125 updateState(CHANNEL_NOTIFICATION_VOLUME, new PercentType(notificationVolumeLevel));
1127 updateState(CHANNEL_NOTIFICATION_VOLUME, UnDefType.UNDEF);
1129 } catch (Exception e) {
1130 this.logger.debug("Handle updateState {} failed: {}", this.getThing().getUID(), e.getMessage(), e);
1132 disableUpdate = false;
1133 throw e; // Rethrow same exception
1137 private void updateEqualizerState() {
1138 if (!this.capabilities.contains("SOUND_SETTINGS")) {
1142 Connection connection = findConnection();
1143 if (connection == null) {
1146 Device device = findDevice();
1147 if (device == null) {
1150 Integer bass = null;
1151 Integer midrange = null;
1152 Integer treble = null;
1154 JsonEqualizer equalizer = connection.getEqualizer(device);
1155 if (equalizer != null) {
1156 bass = equalizer.bass;
1157 midrange = equalizer.mid;
1158 treble = equalizer.treble;
1160 this.lastKnownEqualizer = equalizer;
1161 } catch (IOException | URISyntaxException | HttpException | ConnectionException | InterruptedException e) {
1162 logger.debug("Get equalizer failes", e);
1166 updateState(CHANNEL_EQUALIZER_BASS, new DecimalType(bass));
1168 if (midrange != null) {
1169 updateState(CHANNEL_EQUALIZER_MIDRANGE, new DecimalType(midrange));
1171 if (treble != null) {
1172 updateState(CHANNEL_EQUALIZER_TREBLE, new DecimalType(treble));
1176 private void updateMediaProgress() {
1177 updateMediaProgress(false);
1180 private void updateMediaProgress(boolean updateMediaLength) {
1181 synchronized (progressLock) {
1182 if (mediaStartMs > 0) {
1183 long currentPlayTimeMs = isPlaying ? System.currentTimeMillis() - mediaStartMs : mediaProgressMs;
1184 if (mediaLengthMs > 0) {
1185 int progressPercent = (int) Math.min(100,
1186 Math.round((double) currentPlayTimeMs / (double) mediaLengthMs * 100));
1187 updateState(CHANNEL_MEDIA_PROGRESS, new PercentType(progressPercent));
1189 updateState(CHANNEL_MEDIA_PROGRESS, UnDefType.UNDEF);
1191 updateState(CHANNEL_MEDIA_PROGRESS_TIME, new QuantityType<>(currentPlayTimeMs / 1000, Units.SECOND));
1192 if (updateMediaLength) {
1193 updateState(CHANNEL_MEDIA_LENGTH, new QuantityType<>(mediaLengthMs / 1000, Units.SECOND));
1196 updateState(CHANNEL_MEDIA_PROGRESS, UnDefType.UNDEF);
1197 updateState(CHANNEL_MEDIA_LENGTH, UnDefType.UNDEF);
1198 updateState(CHANNEL_MEDIA_PROGRESS_TIME, UnDefType.UNDEF);
1199 if (updateMediaLength) {
1200 updateState(CHANNEL_MEDIA_LENGTH, UnDefType.UNDEF);
1206 public void handlePushActivity(Activity pushActivity) {
1207 if ("DISCARDED_NON_DEVICE_DIRECTED_INTENT".equals(pushActivity.activityStatus)) {
1210 Description description = pushActivity.parseDescription();
1211 String firstUtteranceId = description.firstUtteranceId;
1212 if (firstUtteranceId == null || firstUtteranceId.isEmpty()
1213 || firstUtteranceId.toLowerCase().startsWith("textclient:")) {
1216 String firstStreamId = description.firstStreamId;
1217 if (firstStreamId == null || firstStreamId.isEmpty()) {
1220 String spokenText = description.summary;
1221 if (spokenText != null && !spokenText.isEmpty()) {
1223 String wakeWordPrefix = this.wakeWord;
1224 if (wakeWordPrefix != null) {
1225 wakeWordPrefix += " ";
1226 if (spokenText.toLowerCase().startsWith(wakeWordPrefix.toLowerCase())) {
1227 spokenText = spokenText.substring(wakeWordPrefix.length());
1231 if (lastSpokenText.isEmpty() || lastSpokenText.equals(spokenText)) {
1232 updateState(CHANNEL_LAST_VOICE_COMMAND, StringType.EMPTY);
1234 lastSpokenText = spokenText;
1235 updateState(CHANNEL_LAST_VOICE_COMMAND, new StringType(spokenText));
1239 public void handlePushCommand(String command, String payload) {
1240 this.logger.debug("Handle push command {}", command);
1242 case "PUSH_VOLUME_CHANGE":
1243 JsonCommandPayloadPushVolumeChange volumeChange = Objects
1244 .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushVolumeChange.class));
1245 Connection connection = this.findConnection();
1246 Integer volumeSetting = volumeChange.volumeSetting;
1247 Boolean muted = volumeChange.isMuted;
1248 if (muted != null && muted) {
1249 updateState(CHANNEL_VOLUME, new PercentType(0));
1251 if (volumeSetting != null && connection != null && !connection.isSequenceNodeQueueRunning()) {
1252 lastKnownVolume = volumeSetting;
1253 updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume));
1256 case "PUSH_EQUALIZER_STATE_CHANGE":
1257 updateEqualizerState();
1260 AccountHandler account = this.account;
1261 Device device = this.device;
1262 if (account != null && device != null) {
1263 this.disableUpdate = false;
1264 updateState(account, device, null, null, null, null, null, null);
1269 public void updateNotifications(ZonedDateTime currentTime, ZonedDateTime now,
1270 @Nullable JsonCommandPayloadPushNotificationChange pushPayload,
1271 List<JsonNotificationResponse> notifications) {
1272 Device device = this.device;
1273 if (device == null) {
1277 ZonedDateTime nextReminder = null;
1278 ZonedDateTime nextAlarm = null;
1279 ZonedDateTime nextMusicAlarm = null;
1280 ZonedDateTime nextTimer = null;
1281 for (JsonNotificationResponse notification : notifications) {
1282 if (Objects.equals(notification.deviceSerialNumber, device.serialNumber)) {
1283 // notification for this device
1284 if ("ON".equals(notification.status)) {
1285 if ("Reminder".equals(notification.type)) {
1286 String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1287 String date = notification.originalDate != null ? notification.originalDate
1288 : ZonedDateTime.now().toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
1289 String time = notification.originalTime != null ? notification.originalTime : "00:00:00";
1290 ZonedDateTime alarmTime = ZonedDateTime.parse(date + "T" + time + offset,
1291 DateTimeFormatter.ISO_DATE_TIME);
1292 String recurringPattern = notification.recurringPattern;
1293 if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
1294 continue; // Ignore recurring entry if alarm time is before now
1296 if (nextReminder == null || alarmTime.isBefore(nextReminder)) {
1297 nextReminder = alarmTime;
1299 } else if ("Timer".equals(notification.type)) {
1300 // use remaining time
1301 ZonedDateTime alarmTime = currentTime.plus(notification.remainingTime, ChronoUnit.MILLIS);
1302 if (nextTimer == null || alarmTime.isBefore(nextTimer)) {
1303 nextTimer = alarmTime;
1305 } else if ("Alarm".equals(notification.type)) {
1306 String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1307 ZonedDateTime alarmTime = ZonedDateTime
1308 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1309 String recurringPattern = notification.recurringPattern;
1310 if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
1311 continue; // Ignore recurring entry if alarm time is before now
1313 if (nextAlarm == null || alarmTime.isBefore(nextAlarm)) {
1314 nextAlarm = alarmTime;
1316 } else if ("MusicAlarm".equals(notification.type)) {
1317 String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1318 ZonedDateTime alarmTime = ZonedDateTime
1319 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1320 String recurringPattern = notification.recurringPattern;
1321 if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
1322 continue; // Ignore recurring entry if alarm time is before now
1324 if (nextMusicAlarm == null || alarmTime.isBefore(nextMusicAlarm)) {
1325 nextMusicAlarm = alarmTime;
1332 updateState(CHANNEL_NEXT_REMINDER, nextReminder == null ? UnDefType.UNDEF : new DateTimeType(nextReminder));
1333 updateState(CHANNEL_NEXT_ALARM, nextAlarm == null ? UnDefType.UNDEF : new DateTimeType(nextAlarm));
1334 updateState(CHANNEL_NEXT_MUSIC_ALARM,
1335 nextMusicAlarm == null ? UnDefType.UNDEF : new DateTimeType(nextMusicAlarm));
1336 updateState(CHANNEL_NEXT_TIMER, nextTimer == null ? UnDefType.UNDEF : new DateTimeType(nextTimer));
1340 public void updateChannelState(String channelId, State state) {
1341 updateState(channelId, state);