2 * Copyright (c) 2010-2021 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;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.amazonechocontrol.internal.Connection;
31 import org.openhab.binding.amazonechocontrol.internal.ConnectionException;
32 import org.openhab.binding.amazonechocontrol.internal.HttpException;
33 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandler;
34 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandlerAnnouncement;
35 import org.openhab.binding.amazonechocontrol.internal.channelhandler.IEchoThingHandler;
36 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity;
37 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity.Description;
38 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm.AscendingAlarmModel;
39 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates;
40 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState;
41 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.PairedDevice;
42 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushNotificationChange;
43 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushVolumeChange;
44 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState.DeviceNotificationState;
45 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device;
46 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonEqualizer;
47 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState;
48 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState.QueueEntry;
49 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider;
50 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse;
51 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound;
52 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState;
53 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo;
54 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.InfoText;
55 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.MainArt;
56 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Progress;
57 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Provider;
58 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Volume;
59 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists;
60 import org.openhab.core.library.types.DateTimeType;
61 import org.openhab.core.library.types.DecimalType;
62 import org.openhab.core.library.types.IncreaseDecreaseType;
63 import org.openhab.core.library.types.NextPreviousType;
64 import org.openhab.core.library.types.OnOffType;
65 import org.openhab.core.library.types.PercentType;
66 import org.openhab.core.library.types.PlayPauseType;
67 import org.openhab.core.library.types.QuantityType;
68 import org.openhab.core.library.types.RewindFastforwardType;
69 import org.openhab.core.library.types.StringType;
70 import org.openhab.core.library.unit.Units;
71 import org.openhab.core.thing.Bridge;
72 import org.openhab.core.thing.ChannelUID;
73 import org.openhab.core.thing.Thing;
74 import org.openhab.core.thing.ThingStatus;
75 import org.openhab.core.thing.ThingUID;
76 import org.openhab.core.thing.binding.BaseThingHandler;
77 import org.openhab.core.types.Command;
78 import org.openhab.core.types.RefreshType;
79 import org.openhab.core.types.State;
80 import org.openhab.core.types.UnDefType;
81 import org.slf4j.Logger;
82 import org.slf4j.LoggerFactory;
84 import com.google.gson.Gson;
87 * The {@link EchoHandler} is responsible for the handling of the echo device
89 * @author Michael Geramb - Initial contribution
92 public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
93 private final Logger logger = LoggerFactory.getLogger(EchoHandler.class);
95 private @Nullable Device device;
96 private Set<String> capabilities = new HashSet<>();
97 private @Nullable AccountHandler account;
98 private @Nullable ScheduledFuture<?> updateStateJob;
99 private @Nullable ScheduledFuture<?> updateProgressJob;
100 private Object progressLock = new Object();
101 private @Nullable String wakeWord;
102 private @Nullable String lastKnownRadioStationId;
103 private @Nullable String lastKnownBluetoothMAC;
104 private @Nullable String lastKnownAmazonMusicId;
105 private String musicProviderId = "TUNEIN";
106 private boolean isPlaying = false;
107 private boolean isPaused = false;
108 private int lastKnownVolume = 25;
109 private int textToSpeechVolume = 0;
110 private @Nullable JsonEqualizer lastKnownEqualizer = null;
111 private @Nullable BluetoothState bluetoothState;
112 private boolean disableUpdate = false;
113 private boolean updateRemind = true;
114 private boolean updateTextToSpeech = true;
115 private boolean updateTextCommand = 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 List<JsonNotificationSound> alarmSounds = List.of();
124 private List<JsonMusicProvider> musicProviders = List.of();
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 this.capabilities = device.getCapabilities();
165 if (!device.online) {
166 updateStatus(ThingStatus.OFFLINE);
169 updateStatus(ThingStatus.ONLINE);
174 public void dispose() {
175 stopCurrentNotification();
176 ScheduledFuture<?> updateStateJob = this.updateStateJob;
177 this.updateStateJob = null;
178 if (updateStateJob != null) {
179 this.disableUpdate = false;
180 updateStateJob.cancel(false);
186 private void stopProgressTimer() {
187 ScheduledFuture<?> updateProgressJob = this.updateProgressJob;
188 this.updateProgressJob = null;
189 if (updateProgressJob != null) {
190 updateProgressJob.cancel(false);
194 public @Nullable BluetoothState findBluetoothState() {
195 return this.bluetoothState;
198 public @Nullable JsonPlaylists findPlaylists() {
199 return this.playLists;
202 public List<JsonNotificationSound> findAlarmSounds() {
203 return this.alarmSounds;
206 public List<JsonMusicProvider> findMusicProviders() {
207 return this.musicProviders;
210 private @Nullable Connection findConnection() {
211 AccountHandler accountHandler = this.account;
212 if (accountHandler != null) {
213 return accountHandler.findConnection();
218 public @Nullable AccountHandler findAccount() {
222 public @Nullable Device findDevice() {
226 public String findSerialNumber() {
227 String id = (String) getConfig().get(DEVICE_PROPERTY_SERIAL_NUMBER);
235 public void handleCommand(ChannelUID channelUID, Command command) {
237 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
238 int waitForUpdate = 1000;
239 boolean needBluetoothRefresh = false;
240 String lastKnownBluetoothMAC = this.lastKnownBluetoothMAC;
242 ScheduledFuture<?> updateStateJob = this.updateStateJob;
243 this.updateStateJob = null;
244 if (updateStateJob != null) {
245 this.disableUpdate = false;
246 updateStateJob.cancel(false);
248 AccountHandler account = this.account;
249 if (account == null) {
252 Connection connection = account.findConnection();
253 if (connection == null) {
256 Device device = this.device;
257 if (device == null) {
261 String channelId = channelUID.getId();
262 for (ChannelHandler channelHandler : channelHandlers) {
263 if (channelHandler.tryHandleCommand(device, connection, channelId, command)) {
269 if (channelId.equals(CHANNEL_PLAYER)) {
270 if (command == PlayPauseType.PAUSE || command == OnOffType.OFF) {
271 connection.command(device, "{\"type\":\"PauseCommand\"}");
272 } else if (command == PlayPauseType.PLAY || command == OnOffType.ON) {
274 connection.command(device, "{\"type\":\"PlayCommand\"}");
276 connection.playMusicVoiceCommand(device, this.musicProviderId, "!");
277 waitForUpdate = 3000;
279 } else if (command == NextPreviousType.NEXT) {
280 connection.command(device, "{\"type\":\"NextCommand\"}");
281 } else if (command == NextPreviousType.PREVIOUS) {
282 connection.command(device, "{\"type\":\"PreviousCommand\"}");
283 } else if (command == RewindFastforwardType.FASTFORWARD) {
284 connection.command(device, "{\"type\":\"ForwardCommand\"}");
285 } else if (command == RewindFastforwardType.REWIND) {
286 connection.command(device, "{\"type\":\"RewindCommand\"}");
289 // Notification commands
290 if (channelId.equals(CHANNEL_NOTIFICATION_VOLUME)) {
291 if (command instanceof PercentType) {
292 int volume = ((PercentType) command).intValue();
293 connection.notificationVolume(device, volume);
294 this.notificationVolumeLevel = volume;
296 account.forceCheckData();
299 if (channelId.equals(CHANNEL_ASCENDING_ALARM)) {
300 if (command == OnOffType.OFF) {
301 connection.ascendingAlarm(device, false);
302 this.ascendingAlarm = false;
304 account.forceCheckData();
306 if (command == OnOffType.ON) {
307 connection.ascendingAlarm(device, true);
308 this.ascendingAlarm = true;
310 account.forceCheckData();
313 // Media progress commands
314 Long mediaPosition = null;
315 if (channelId.equals(CHANNEL_MEDIA_PROGRESS)) {
316 if (command instanceof PercentType) {
317 PercentType value = (PercentType) command;
318 int percent = value.intValue();
319 mediaPosition = Math.round((mediaLengthMs / 1000d) * (percent / 100d));
322 if (channelId.equals(CHANNEL_MEDIA_PROGRESS_TIME)) {
323 if (command instanceof DecimalType) {
324 DecimalType value = (DecimalType) command;
325 mediaPosition = value.longValue();
327 if (command instanceof QuantityType<?>) {
328 QuantityType<?> value = (QuantityType<?>) command;
330 QuantityType<?> seconds = value.toUnit(Units.SECOND);
331 if (seconds != null) {
332 mediaPosition = seconds.longValue();
336 if (mediaPosition != null) {
338 synchronized (progressLock) {
339 String seekCommand = "{\"type\":\"SeekCommand\",\"mediaPosition\":" + mediaPosition
340 + ",\"contentFocusClientId\":null}";
341 connection.command(device, seekCommand);
342 connection.command(device, seekCommand); // Must be sent twice, the first one is ignored sometimes
343 this.mediaProgressMs = mediaPosition * 1000;
344 mediaStartMs = System.currentTimeMillis() - this.mediaProgressMs;
345 updateMediaProgress(false);
349 if (channelId.equals(CHANNEL_VOLUME)) {
350 Integer volume = null;
351 if (command instanceof PercentType) {
352 PercentType value = (PercentType) command;
353 volume = value.intValue();
354 } else if (command == OnOffType.OFF) {
356 } else if (command == OnOffType.ON) {
357 volume = lastKnownVolume;
358 } else if (command == IncreaseDecreaseType.INCREASE) {
359 if (lastKnownVolume < 100) {
361 volume = lastKnownVolume;
363 } else if (command == IncreaseDecreaseType.DECREASE) {
364 if (lastKnownVolume > 0) {
366 volume = lastKnownVolume;
369 if (volume != null) {
370 if ("WHA".equals(device.deviceFamily)) {
371 connection.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + volume
372 + ",\"contentFocusClientId\":\"Default\"}");
374 connection.volume(device, volume);
376 lastKnownVolume = volume;
377 updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume));
381 // equalizer commands
382 if (channelId.equals(CHANNEL_EQUALIZER_BASS) || channelId.equals(CHANNEL_EQUALIZER_MIDRANGE)
383 || channelId.equals(CHANNEL_EQUALIZER_TREBLE)) {
384 if (handleEqualizerCommands(channelId, command, connection, device)) {
390 if (channelId.equals(CHANNEL_SHUFFLE)) {
391 if (command instanceof OnOffType) {
392 OnOffType value = (OnOffType) command;
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) {
428 String address = ((StringType) command).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) {
565 PercentType value = (PercentType) command;
566 textToSpeechVolume = value.intValue();
567 } else if (command == OnOffType.OFF) {
568 textToSpeechVolume = 0;
569 } else if (command == OnOffType.ON) {
570 textToSpeechVolume = lastKnownVolume;
571 } else if (command == IncreaseDecreaseType.INCREASE) {
572 if (textToSpeechVolume < 100) {
573 textToSpeechVolume++;
575 } else if (command == IncreaseDecreaseType.DECREASE) {
576 if (textToSpeechVolume > 0) {
577 textToSpeechVolume--;
580 this.updateState(channelId, new PercentType(textToSpeechVolume));
582 if (channelId.equals(CHANNEL_TEXT_COMMAND)) {
583 if (command instanceof StringType) {
584 String text = command.toFullString();
585 if (!text.isEmpty()) {
586 waitForUpdate = 1000;
587 updateTextCommand = true;
588 startTextCommand(connection, device, text);
592 if (channelId.equals(CHANNEL_LAST_VOICE_COMMAND)) {
593 if (command instanceof StringType) {
594 String text = command.toFullString();
595 if (!text.isEmpty()) {
597 startTextToSpeech(connection, device, text);
601 if (channelId.equals(CHANNEL_START_COMMAND)) {
602 if (command instanceof StringType) {
603 String commandText = command.toFullString();
604 if (!commandText.isEmpty()) {
605 updateStartCommand = true;
606 if (commandText.startsWith(FLASH_BRIEFING_COMMAND_PREFIX)) {
607 // Handle custom flashbriefings commands
608 String flashBriefingId = commandText.substring(FLASH_BRIEFING_COMMAND_PREFIX.length());
609 for (FlashBriefingProfileHandler flashBriefingHandler : account
610 .getFlashBriefingProfileHandlers()) {
611 ThingUID flashBriefingUid = flashBriefingHandler.getThing().getUID();
612 if (flashBriefingId.equals(flashBriefingHandler.getThing().getUID().getId())) {
613 flashBriefingHandler.handleCommand(
614 new ChannelUID(flashBriefingUid, CHANNEL_PLAY_ON_DEVICE),
615 new StringType(device.serialNumber));
620 // Handle standard commands
621 if (!commandText.startsWith("Alexa.")) {
622 commandText = "Alexa." + commandText + ".Play";
624 waitForUpdate = 1000;
625 connection.executeSequenceCommand(device, commandText, Map.of());
630 if (channelId.equals(CHANNEL_START_ROUTINE)) {
631 if (command instanceof StringType) {
632 String utterance = command.toFullString();
633 if (!utterance.isEmpty()) {
634 waitForUpdate = 1000;
635 updateRoutine = true;
636 connection.startRoutine(device, utterance);
640 if (waitForUpdate < 0) {
643 // force update of the state
644 this.disableUpdate = true;
645 final boolean bluetoothRefresh = needBluetoothRefresh;
646 Runnable doRefresh = () -> {
647 this.disableUpdate = false;
648 BluetoothState state = null;
649 if (bluetoothRefresh) {
650 JsonBluetoothStates states;
651 states = connection.getBluetoothConnectionStates();
652 if (states != null) {
653 state = states.findStateByDevice(device);
657 updateState(account, device, state, null, null, null, null, null);
659 if (command instanceof RefreshType) {
661 account.forceCheckData();
663 if (waitForUpdate == 0) {
666 this.updateStateJob = scheduler.schedule(doRefresh, waitForUpdate, TimeUnit.MILLISECONDS);
668 } catch (IOException | URISyntaxException | InterruptedException e) {
669 logger.info("handleCommand fails", e);
673 private boolean handleEqualizerCommands(String channelId, Command command, Connection connection, Device device)
674 throws URISyntaxException {
675 if (command instanceof RefreshType) {
676 this.lastKnownEqualizer = null;
678 if (command instanceof DecimalType) {
679 DecimalType value = (DecimalType) command;
680 if (this.lastKnownEqualizer == null) {
681 updateEqualizerState();
683 JsonEqualizer lastKnownEqualizer = this.lastKnownEqualizer;
684 if (lastKnownEqualizer != null) {
685 JsonEqualizer newEqualizerSetting = lastKnownEqualizer.createClone();
686 if (channelId.equals(CHANNEL_EQUALIZER_BASS)) {
687 newEqualizerSetting.bass = value.intValue();
689 if (channelId.equals(CHANNEL_EQUALIZER_MIDRANGE)) {
690 newEqualizerSetting.mid = value.intValue();
692 if (channelId.equals(CHANNEL_EQUALIZER_TREBLE)) {
693 newEqualizerSetting.treble = value.intValue();
696 connection.setEqualizer(device, newEqualizerSetting);
698 } catch (HttpException | IOException | ConnectionException | InterruptedException e) {
699 logger.debug("Update equalizer failed", e);
700 this.lastKnownEqualizer = null;
707 private void startTextToSpeech(Connection connection, Device device, String text)
708 throws IOException, URISyntaxException {
709 Integer volume = null;
710 if (textToSpeechVolume != 0) {
711 volume = textToSpeechVolume;
713 connection.textToSpeech(device, text, volume, lastKnownVolume);
716 private void startTextCommand(Connection connection, Device device, String text)
717 throws IOException, URISyntaxException {
718 Integer volume = null;
719 if (textToSpeechVolume != 0) {
720 volume = textToSpeechVolume;
722 connection.textCommand(device, text, volume, lastKnownVolume);
726 public void startAnnouncement(Device device, String speak, String bodyText, @Nullable String title,
727 @Nullable Integer volume) throws IOException, URISyntaxException {
728 Connection connection = this.findConnection();
729 if (connection == null) {
732 if (volume == null && textToSpeechVolume != 0) {
733 volume = textToSpeechVolume;
735 if (volume != null && volume < 0) {
736 volume = null; // the meaning of negative values is 'do not use'. The api requires null in this case.
738 connection.announcement(device, speak, bodyText, title, volume, lastKnownVolume);
741 private void stopCurrentNotification() {
742 ScheduledFuture<?> currentNotifcationUpdateTimer = this.currentNotifcationUpdateTimer;
743 if (currentNotifcationUpdateTimer != null) {
744 this.currentNotifcationUpdateTimer = null;
745 currentNotifcationUpdateTimer.cancel(true);
747 JsonNotificationResponse currentNotification = this.currentNotification;
748 if (currentNotification != null) {
749 this.currentNotification = null;
750 Connection currentConnection = this.findConnection();
751 if (currentConnection != null) {
753 currentConnection.stopNotification(currentNotification);
754 } catch (IOException | URISyntaxException | InterruptedException e) {
755 logger.warn("Stop notification failed", e);
761 private void updateNotificationTimerState() {
762 boolean stopCurrentNotification = true;
763 JsonNotificationResponse currentNotification = this.currentNotification;
765 if (currentNotification != null) {
766 Connection currentConnection = this.findConnection();
767 if (currentConnection != null) {
768 JsonNotificationResponse newState = currentConnection.getNotificationState(currentNotification);
769 if (newState != null && "ON".equals(newState.status)) {
770 stopCurrentNotification = false;
774 } catch (IOException | URISyntaxException | InterruptedException e) {
775 logger.warn("update notification state fails", e);
777 if (stopCurrentNotification) {
778 if (currentNotification != null) {
779 String type = currentNotification.type;
781 if (type.equals("Reminder")) {
782 updateState(CHANNEL_REMIND, StringType.EMPTY);
783 updateRemind = false;
785 if (type.equals("Alarm")) {
786 updateState(CHANNEL_PLAY_ALARM_SOUND, StringType.EMPTY);
791 stopCurrentNotification();
795 public void updateState(AccountHandler accountHandler, @Nullable Device device,
796 @Nullable BluetoothState bluetoothState, @Nullable DeviceNotificationState deviceNotificationState,
797 @Nullable AscendingAlarmModel ascendingAlarmModel, @Nullable JsonPlaylists playlists,
798 @Nullable List<JsonNotificationSound> alarmSounds, @Nullable List<JsonMusicProvider> musicProviders) {
800 this.logger.debug("Handle updateState {}", this.getThing().getUID());
802 if (deviceNotificationState != null) {
803 notificationVolumeLevel = deviceNotificationState.volumeLevel;
805 if (ascendingAlarmModel != null) {
806 ascendingAlarm = ascendingAlarmModel.ascendingAlarmEnabled;
808 if (playlists != null) {
809 this.playLists = playlists;
811 if (alarmSounds != null) {
812 this.alarmSounds = alarmSounds;
814 if (musicProviders != null) {
815 this.musicProviders = musicProviders;
817 if (!setDeviceAndUpdateThingState(accountHandler, device, null)) {
818 this.logger.debug("Handle updateState {} aborted: Not online", this.getThing().getUID());
821 if (device == null) {
822 this.logger.debug("Handle updateState {} aborted: No device", this.getThing().getUID());
826 if (this.disableUpdate) {
827 this.logger.debug("Handle updateState {} aborted: Disabled", this.getThing().getUID());
830 Connection connection = this.findConnection();
831 if (connection == null) {
835 if (this.lastKnownEqualizer == null) {
836 updateEqualizerState();
839 PlayerInfo playerInfo = null;
840 Provider provider = null;
841 InfoText infoText = null;
842 MainArt mainArt = null;
843 String musicProviderId = null;
844 Progress progress = null;
846 JsonPlayerState playerState = connection.getPlayer(device);
847 if (playerState != null) {
848 playerInfo = playerState.playerInfo;
849 if (playerInfo != null) {
850 infoText = playerInfo.infoText;
851 if (infoText == null) {
852 infoText = playerInfo.miniInfoText;
854 mainArt = playerInfo.mainArt;
855 provider = playerInfo.provider;
856 if (provider != null) {
857 musicProviderId = provider.providerName;
858 // Map the music provider id to the one used for starting music with voice command
859 if (musicProviderId != null) {
860 musicProviderId = musicProviderId.toUpperCase();
862 if (musicProviderId.equals("AMAZON MUSIC")) {
863 musicProviderId = "AMAZON_MUSIC";
865 if (musicProviderId.equals("CLOUD_PLAYER")) {
866 musicProviderId = "AMAZON_MUSIC";
868 if (musicProviderId.startsWith("TUNEIN")) {
869 musicProviderId = "TUNEIN";
871 if (musicProviderId.startsWith("IHEARTRADIO")) {
872 musicProviderId = "I_HEART_RADIO";
874 if (musicProviderId.equals("APPLE") && musicProviderId.contains("MUSIC")) {
875 musicProviderId = "APPLE_MUSIC";
879 progress = playerInfo.progress;
882 } catch (HttpException e) {
883 if (e.getCode() != 400) {
884 logger.info("getPlayer fails", e);
886 } catch (IOException | URISyntaxException | InterruptedException e) {
887 logger.info("getPlayer fails", e);
890 isPlaying = (playerInfo != null && "PLAYING".equals(playerInfo.state));
892 isPaused = (playerInfo != null && "PAUSED".equals(playerInfo.state));
893 synchronized (progressLock) {
894 Boolean showTime = null;
895 Long mediaLength = null;
896 Long mediaProgress = null;
897 if (progress != null) {
898 showTime = progress.showTiming;
899 mediaLength = progress.mediaLength;
900 mediaProgress = progress.mediaProgress;
902 if (showTime != null && showTime && mediaProgress != null && mediaLength != null) {
903 mediaProgressMs = mediaProgress * 1000;
904 mediaLengthMs = mediaLength * 1000;
905 mediaStartMs = System.currentTimeMillis() - mediaProgressMs;
907 if (updateProgressJob == null) {
908 updateProgressJob = scheduler.scheduleWithFixedDelay(this::updateMediaProgress, 1000, 1000,
909 TimeUnit.MILLISECONDS);
920 updateMediaProgress(true);
923 JsonMediaState mediaState = null;
925 if ("AMAZON_MUSIC".equalsIgnoreCase(musicProviderId) || "TUNEIN".equalsIgnoreCase(musicProviderId)) {
926 mediaState = connection.getMediaState(device);
928 } catch (HttpException e) {
929 if (e.getCode() == 400) {
930 updateState(CHANNEL_RADIO_STATION_ID, StringType.EMPTY);
932 logger.info("getMediaState fails", e);
934 } catch (IOException | URISyntaxException | InterruptedException e) {
935 logger.info("getMediaState fails", e);
938 // handle music provider id
939 if (provider != null && isPlaying) {
940 if (musicProviderId != null) {
941 this.musicProviderId = musicProviderId;
945 // handle amazon music
946 String amazonMusicTrackId = "";
947 String amazonMusicPlayListId = "";
948 boolean amazonMusic = false;
949 if (mediaState != null) {
950 String contentId = mediaState.contentId;
951 if (isPlaying && "CLOUD_PLAYER".equals(mediaState.providerId) && contentId != null
952 && !contentId.isEmpty()) {
953 amazonMusicTrackId = contentId;
954 lastKnownAmazonMusicId = amazonMusicTrackId;
960 String bluetoothMAC = "";
961 String bluetoothDeviceName = "";
962 boolean bluetoothIsConnected = false;
963 if (bluetoothState != null) {
964 this.bluetoothState = bluetoothState;
965 for (PairedDevice paired : bluetoothState.getPairedDeviceList()) {
966 String pairedAddress = paired.address;
967 if (paired.connected && pairedAddress != null) {
968 bluetoothIsConnected = true;
969 bluetoothMAC = pairedAddress;
970 bluetoothDeviceName = paired.friendlyName;
971 if (bluetoothDeviceName == null || bluetoothDeviceName.isEmpty()) {
972 bluetoothDeviceName = pairedAddress;
979 if (!bluetoothMAC.isEmpty()) {
980 lastKnownBluetoothMAC = bluetoothMAC;
984 boolean isRadio = false;
985 String radioStationId = "";
986 if (mediaState != null) {
987 radioStationId = Objects.requireNonNullElse(mediaState.radioStationId, "");
988 if (!radioStationId.isEmpty()) {
989 lastKnownRadioStationId = radioStationId;
990 if ("TUNEIN".equalsIgnoreCase(musicProviderId)) {
992 if (!"PLAYING".equals(mediaState.currentState)) {
999 // handle title, subtitle, imageUrl
1001 String subTitle1 = "";
1002 String subTitle2 = "";
1003 String imageUrl = "";
1004 if (infoText != null) {
1005 if (infoText.title != null) {
1006 title = infoText.title;
1008 if (infoText.subText1 != null) {
1009 subTitle1 = infoText.subText1;
1012 if (infoText.subText2 != null) {
1013 subTitle2 = infoText.subText2;
1016 if (mainArt != null) {
1017 if (mainArt.url != null) {
1018 imageUrl = mainArt.url;
1021 if (mediaState != null) {
1022 List<QueueEntry> queueEntries = Objects.requireNonNullElse(mediaState.queue, List.of());
1023 if (!queueEntries.isEmpty()) {
1024 QueueEntry entry = queueEntries.get(0);
1026 if ((imageUrl == null || imageUrl.isEmpty()) && entry.imageURL != null) {
1027 imageUrl = entry.imageURL;
1029 if ((subTitle1 == null || subTitle1.isEmpty()) && entry.radioStationSlogan != null) {
1030 subTitle1 = entry.radioStationSlogan;
1032 if ((subTitle2 == null || subTitle2.isEmpty()) && entry.radioStationLocation != null) {
1033 subTitle2 = entry.radioStationLocation;
1041 String providerDisplayName = "";
1042 if (provider != null) {
1043 if (provider.providerDisplayName != null) {
1044 providerDisplayName = Objects.requireNonNullElse(provider.providerDisplayName, providerDisplayName);
1046 String providerName = provider.providerName;
1047 if (providerName != null && !providerName.isEmpty() && providerDisplayName.isEmpty()) {
1048 providerDisplayName = provider.providerName;
1053 Integer volume = null;
1054 if (!connection.isSequenceNodeQueueRunning()) {
1055 if (mediaState != null) {
1056 volume = mediaState.volume;
1058 if (playerInfo != null && volume == null) {
1059 Volume volumnInfo = playerInfo.volume;
1060 if (volumnInfo != null) {
1061 volume = volumnInfo.volume;
1064 if (volume != null && volume > 0) {
1065 lastKnownVolume = volume;
1067 if (volume == null) {
1068 volume = lastKnownVolume;
1072 if (updateRemind && currentNotifcationUpdateTimer == null) {
1073 updateRemind = false;
1074 updateState(CHANNEL_REMIND, StringType.EMPTY);
1076 if (updateAlarm && currentNotifcationUpdateTimer == null) {
1077 updateAlarm = false;
1078 updateState(CHANNEL_PLAY_ALARM_SOUND, StringType.EMPTY);
1080 if (updateRoutine) {
1081 updateRoutine = false;
1082 updateState(CHANNEL_START_ROUTINE, StringType.EMPTY);
1084 if (updateTextToSpeech) {
1085 updateTextToSpeech = false;
1086 updateState(CHANNEL_TEXT_TO_SPEECH, StringType.EMPTY);
1088 if (updateTextCommand) {
1089 updateTextCommand = false;
1090 updateState(CHANNEL_TEXT_COMMAND, StringType.EMPTY);
1092 if (updatePlayMusicVoiceCommand) {
1093 updatePlayMusicVoiceCommand = false;
1094 updateState(CHANNEL_PLAY_MUSIC_VOICE_COMMAND, StringType.EMPTY);
1096 if (updateStartCommand) {
1097 updateStartCommand = false;
1098 updateState(CHANNEL_START_COMMAND, StringType.EMPTY);
1101 updateState(CHANNEL_MUSIC_PROVIDER_ID, new StringType(musicProviderId));
1102 updateState(CHANNEL_AMAZON_MUSIC_TRACK_ID, new StringType(amazonMusicTrackId));
1103 updateState(CHANNEL_AMAZON_MUSIC, isPlaying && amazonMusic ? OnOffType.ON : OnOffType.OFF);
1104 updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID, new StringType(amazonMusicPlayListId));
1105 updateState(CHANNEL_RADIO_STATION_ID, new StringType(radioStationId));
1106 updateState(CHANNEL_RADIO, isPlaying && isRadio ? OnOffType.ON : OnOffType.OFF);
1107 updateState(CHANNEL_PROVIDER_DISPLAY_NAME, new StringType(providerDisplayName));
1108 updateState(CHANNEL_PLAYER, isPlaying ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
1109 updateState(CHANNEL_IMAGE_URL, new StringType(imageUrl));
1110 updateState(CHANNEL_TITLE, new StringType(title));
1111 if (volume != null) {
1112 updateState(CHANNEL_VOLUME, new PercentType(volume));
1114 updateState(CHANNEL_SUBTITLE1, new StringType(subTitle1));
1115 updateState(CHANNEL_SUBTITLE2, new StringType(subTitle2));
1116 if (bluetoothState != null) {
1117 updateState(CHANNEL_BLUETOOTH, bluetoothIsConnected ? OnOffType.ON : OnOffType.OFF);
1118 updateState(CHANNEL_BLUETOOTH_MAC, new StringType(bluetoothMAC));
1119 updateState(CHANNEL_BLUETOOTH_DEVICE_NAME, new StringType(bluetoothDeviceName));
1122 updateState(CHANNEL_ASCENDING_ALARM,
1123 ascendingAlarm != null ? (ascendingAlarm ? OnOffType.ON : OnOffType.OFF) : UnDefType.UNDEF);
1125 final Integer notificationVolumeLevel = this.notificationVolumeLevel;
1126 if (notificationVolumeLevel != null) {
1127 updateState(CHANNEL_NOTIFICATION_VOLUME, new PercentType(notificationVolumeLevel));
1129 updateState(CHANNEL_NOTIFICATION_VOLUME, UnDefType.UNDEF);
1131 } catch (Exception e) {
1132 this.logger.debug("Handle updateState {} failed: {}", this.getThing().getUID(), e.getMessage(), e);
1134 disableUpdate = false;
1135 throw e; // Rethrow same exception
1139 private void updateEqualizerState() {
1140 if (!this.capabilities.contains("SOUND_SETTINGS")) {
1144 Connection connection = findConnection();
1145 if (connection == null) {
1148 Device device = findDevice();
1149 if (device == null) {
1152 Integer bass = null;
1153 Integer midrange = null;
1154 Integer treble = null;
1156 JsonEqualizer equalizer = connection.getEqualizer(device);
1157 if (equalizer != null) {
1158 bass = equalizer.bass;
1159 midrange = equalizer.mid;
1160 treble = equalizer.treble;
1162 this.lastKnownEqualizer = equalizer;
1163 } catch (IOException | URISyntaxException | HttpException | ConnectionException | InterruptedException e) {
1164 logger.debug("Get equalizer failes", e);
1168 updateState(CHANNEL_EQUALIZER_BASS, new DecimalType(bass));
1170 if (midrange != null) {
1171 updateState(CHANNEL_EQUALIZER_MIDRANGE, new DecimalType(midrange));
1173 if (treble != null) {
1174 updateState(CHANNEL_EQUALIZER_TREBLE, new DecimalType(treble));
1178 private void updateMediaProgress() {
1179 updateMediaProgress(false);
1182 private void updateMediaProgress(boolean updateMediaLength) {
1183 synchronized (progressLock) {
1184 if (mediaStartMs > 0) {
1185 long currentPlayTimeMs = isPlaying ? System.currentTimeMillis() - mediaStartMs : mediaProgressMs;
1186 if (mediaLengthMs > 0) {
1187 int progressPercent = (int) Math.min(100,
1188 Math.round((double) currentPlayTimeMs / (double) mediaLengthMs * 100));
1189 updateState(CHANNEL_MEDIA_PROGRESS, new PercentType(progressPercent));
1191 updateState(CHANNEL_MEDIA_PROGRESS, UnDefType.UNDEF);
1193 updateState(CHANNEL_MEDIA_PROGRESS_TIME, new QuantityType<>(currentPlayTimeMs / 1000, Units.SECOND));
1194 if (updateMediaLength) {
1195 updateState(CHANNEL_MEDIA_LENGTH, new QuantityType<>(mediaLengthMs / 1000, Units.SECOND));
1198 updateState(CHANNEL_MEDIA_PROGRESS, UnDefType.UNDEF);
1199 updateState(CHANNEL_MEDIA_LENGTH, UnDefType.UNDEF);
1200 updateState(CHANNEL_MEDIA_PROGRESS_TIME, UnDefType.UNDEF);
1201 if (updateMediaLength) {
1202 updateState(CHANNEL_MEDIA_LENGTH, UnDefType.UNDEF);
1208 public void handlePushActivity(Activity pushActivity) {
1209 if ("DISCARDED_NON_DEVICE_DIRECTED_INTENT".equals(pushActivity.activityStatus)) {
1212 Description description = pushActivity.parseDescription();
1213 String firstUtteranceId = description.firstUtteranceId;
1214 if (firstUtteranceId == null || firstUtteranceId.isEmpty()
1215 || firstUtteranceId.toLowerCase().startsWith("textclient:")) {
1218 String firstStreamId = description.firstStreamId;
1219 if (firstStreamId == null || firstStreamId.isEmpty()) {
1222 String spokenText = description.summary;
1223 if (spokenText != null && !spokenText.isEmpty()) {
1225 String wakeWordPrefix = this.wakeWord;
1226 if (wakeWordPrefix != null) {
1227 wakeWordPrefix += " ";
1228 if (spokenText.toLowerCase().startsWith(wakeWordPrefix.toLowerCase())) {
1229 spokenText = spokenText.substring(wakeWordPrefix.length());
1233 if (lastSpokenText.isEmpty() || lastSpokenText.equals(spokenText)) {
1234 updateState(CHANNEL_LAST_VOICE_COMMAND, StringType.EMPTY);
1236 lastSpokenText = spokenText;
1237 updateState(CHANNEL_LAST_VOICE_COMMAND, new StringType(spokenText));
1241 public void handlePushCommand(String command, String payload) {
1242 this.logger.debug("Handle push command {}", command);
1244 case "PUSH_VOLUME_CHANGE":
1245 JsonCommandPayloadPushVolumeChange volumeChange = Objects
1246 .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushVolumeChange.class));
1247 Connection connection = this.findConnection();
1248 Integer volumeSetting = volumeChange.volumeSetting;
1249 Boolean muted = volumeChange.isMuted;
1250 if (muted != null && muted) {
1251 updateState(CHANNEL_VOLUME, new PercentType(0));
1253 if (volumeSetting != null && connection != null && !connection.isSequenceNodeQueueRunning()) {
1254 lastKnownVolume = volumeSetting;
1255 updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume));
1258 case "PUSH_EQUALIZER_STATE_CHANGE":
1259 updateEqualizerState();
1262 AccountHandler account = this.account;
1263 Device device = this.device;
1264 if (account != null && device != null) {
1265 this.disableUpdate = false;
1266 updateState(account, device, null, null, null, null, null, null);
1271 public void updateNotifications(ZonedDateTime currentTime, ZonedDateTime now,
1272 @Nullable JsonCommandPayloadPushNotificationChange pushPayload,
1273 List<JsonNotificationResponse> notifications) {
1274 Device device = this.device;
1275 if (device == null) {
1279 ZonedDateTime nextReminder = null;
1280 ZonedDateTime nextAlarm = null;
1281 ZonedDateTime nextMusicAlarm = null;
1282 ZonedDateTime nextTimer = null;
1283 for (JsonNotificationResponse notification : notifications) {
1284 if (Objects.equals(notification.deviceSerialNumber, device.serialNumber)) {
1285 // notification for this device
1286 if ("ON".equals(notification.status)) {
1287 if ("Reminder".equals(notification.type)) {
1288 String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1289 String date = notification.originalDate != null ? notification.originalDate
1290 : ZonedDateTime.now().toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
1291 String time = notification.originalTime != null ? notification.originalTime : "00:00:00";
1292 ZonedDateTime alarmTime = ZonedDateTime.parse(date + "T" + time + offset,
1293 DateTimeFormatter.ISO_DATE_TIME);
1294 String recurringPattern = notification.recurringPattern;
1295 if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
1296 continue; // Ignore recurring entry if alarm time is before now
1298 if (nextReminder == null || alarmTime.isBefore(nextReminder)) {
1299 nextReminder = alarmTime;
1301 } else if ("Timer".equals(notification.type)) {
1302 // use remaining time
1303 ZonedDateTime alarmTime = currentTime.plus(notification.remainingTime, ChronoUnit.MILLIS);
1304 if (nextTimer == null || alarmTime.isBefore(nextTimer)) {
1305 nextTimer = alarmTime;
1307 } else if ("Alarm".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 (nextAlarm == null || alarmTime.isBefore(nextAlarm)) {
1316 nextAlarm = alarmTime;
1318 } else if ("MusicAlarm".equals(notification.type)) {
1319 String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1320 ZonedDateTime alarmTime = ZonedDateTime
1321 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1322 String recurringPattern = notification.recurringPattern;
1323 if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
1324 continue; // Ignore recurring entry if alarm time is before now
1326 if (nextMusicAlarm == null || alarmTime.isBefore(nextMusicAlarm)) {
1327 nextMusicAlarm = alarmTime;
1334 updateState(CHANNEL_NEXT_REMINDER, nextReminder == null ? UnDefType.UNDEF : new DateTimeType(nextReminder));
1335 updateState(CHANNEL_NEXT_ALARM, nextAlarm == null ? UnDefType.UNDEF : new DateTimeType(nextAlarm));
1336 updateState(CHANNEL_NEXT_MUSIC_ALARM,
1337 nextMusicAlarm == null ? UnDefType.UNDEF : new DateTimeType(nextMusicAlarm));
1338 updateState(CHANNEL_NEXT_TIMER, nextTimer == null ? UnDefType.UNDEF : new DateTimeType(nextTimer));
1342 public void updateChannelState(String channelId, State state) {
1343 updateState(channelId, state);