2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.amazonechocontrol.internal.handler;
15 import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*;
17 import java.io.IOException;
18 import java.net.URISyntaxException;
19 import java.time.Instant;
20 import java.time.ZoneId;
21 import java.time.ZonedDateTime;
22 import java.time.temporal.ChronoUnit;
23 import java.util.ArrayList;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Objects;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30 import java.util.stream.Collectors;
31 import java.util.stream.Stream;
33 import org.apache.commons.lang.StringUtils;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.openhab.binding.amazonechocontrol.internal.Connection;
37 import org.openhab.binding.amazonechocontrol.internal.ConnectionException;
38 import org.openhab.binding.amazonechocontrol.internal.HttpException;
39 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandler;
40 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandlerAnnouncement;
41 import org.openhab.binding.amazonechocontrol.internal.channelhandler.IEchoThingHandler;
42 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity;
43 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity.Description;
44 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm.AscendingAlarmModel;
45 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates;
46 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState;
47 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.PairedDevice;
48 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushNotificationChange;
49 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushVolumeChange;
50 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState.DeviceNotificationState;
51 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device;
52 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonEqualizer;
53 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState;
54 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState.QueueEntry;
55 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider;
56 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse;
57 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound;
58 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState;
59 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo;
60 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.InfoText;
61 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.MainArt;
62 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Progress;
63 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Provider;
64 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Volume;
65 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists;
66 import org.openhab.core.library.types.DateTimeType;
67 import org.openhab.core.library.types.DecimalType;
68 import org.openhab.core.library.types.IncreaseDecreaseType;
69 import org.openhab.core.library.types.NextPreviousType;
70 import org.openhab.core.library.types.OnOffType;
71 import org.openhab.core.library.types.PercentType;
72 import org.openhab.core.library.types.PlayPauseType;
73 import org.openhab.core.library.types.QuantityType;
74 import org.openhab.core.library.types.RewindFastforwardType;
75 import org.openhab.core.library.types.StringType;
76 import org.openhab.core.library.unit.SmartHomeUnits;
77 import org.openhab.core.thing.Bridge;
78 import org.openhab.core.thing.ChannelUID;
79 import org.openhab.core.thing.Thing;
80 import org.openhab.core.thing.ThingStatus;
81 import org.openhab.core.thing.ThingUID;
82 import org.openhab.core.thing.binding.BaseThingHandler;
83 import org.openhab.core.types.Command;
84 import org.openhab.core.types.RefreshType;
85 import org.openhab.core.types.State;
86 import org.openhab.core.types.UnDefType;
87 import org.slf4j.Logger;
88 import org.slf4j.LoggerFactory;
90 import com.google.gson.Gson;
93 * The {@link EchoHandler} is responsible for the handling of the echo device
95 * @author Michael Geramb - Initial contribution
98 public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
100 private final Logger logger = LoggerFactory.getLogger(EchoHandler.class);
102 private @Nullable Device device;
103 private Set<String> capabilities = new HashSet<>();
104 private @Nullable AccountHandler account;
105 private @Nullable ScheduledFuture<?> updateStateJob;
106 private @Nullable ScheduledFuture<?> updateProgressJob;
107 private Object progressLock = new Object();
108 private @Nullable String wakeWord;
109 private @Nullable String lastKnownRadioStationId;
110 private @Nullable String lastKnownBluetoothMAC;
111 private @Nullable String lastKnownAmazonMusicId;
112 private String musicProviderId = "TUNEIN";
113 private boolean isPlaying = false;
114 private boolean isPaused = false;
115 private int lastKnownVolume = 25;
116 private int textToSpeechVolume = 0;
117 private @Nullable JsonEqualizer lastKnownEqualizer = null;
118 private @Nullable BluetoothState bluetoothState;
119 private boolean disableUpdate = false;
120 private boolean updateRemind = true;
121 private boolean updateTextToSpeech = true;
122 private boolean updateAlarm = true;
123 private boolean updateRoutine = true;
124 private boolean updatePlayMusicVoiceCommand = true;
125 private boolean updateStartCommand = true;
126 private @Nullable Integer notificationVolumeLevel;
127 private @Nullable Boolean ascendingAlarm;
128 private @Nullable JsonPlaylists playLists;
129 private @Nullable JsonNotificationSound @Nullable [] alarmSounds;
130 private @Nullable List<JsonMusicProvider> musicProviders;
131 private List<ChannelHandler> channelHandlers = new ArrayList<>();
133 private @Nullable JsonNotificationResponse currentNotification;
134 private @Nullable ScheduledFuture<?> currentNotifcationUpdateTimer;
136 long mediaProgressMs;
138 String lastSpokenText = "";
140 public EchoHandler(Thing thing, Gson gson) {
143 channelHandlers.add(new ChannelHandlerAnnouncement(this, this.gson));
147 public void initialize() {
148 logger.debug("Amazon Echo Control Binding initialized");
149 Bridge bridge = this.getBridge();
150 if (bridge != null) {
151 AccountHandler account = (AccountHandler) bridge.getHandler();
152 if (account != null) {
153 setDeviceAndUpdateThingState(account, this.device, null);
154 account.addEchoHandler(this);
159 public boolean setDeviceAndUpdateThingState(AccountHandler accountHandler, @Nullable Device device,
160 @Nullable String wakeWord) {
161 this.account = accountHandler;
162 if (wakeWord != null) {
163 this.wakeWord = wakeWord;
165 if (device == null) {
166 updateStatus(ThingStatus.UNKNOWN);
169 this.device = device;
170 String[] capabilities = device.capabilities;
171 if (capabilities != null) {
172 this.capabilities = Stream.of(capabilities).filter(Objects::nonNull).collect(Collectors.toSet());
174 if (!device.online) {
175 updateStatus(ThingStatus.OFFLINE);
178 updateStatus(ThingStatus.ONLINE);
183 public void dispose() {
184 stopCurrentNotification();
185 ScheduledFuture<?> updateStateJob = this.updateStateJob;
186 this.updateStateJob = null;
187 if (updateStateJob != null) {
188 this.disableUpdate = false;
189 updateStateJob.cancel(false);
195 private void stopProgressTimer() {
196 ScheduledFuture<?> updateProgressJob = this.updateProgressJob;
197 this.updateProgressJob = null;
198 if (updateProgressJob != null) {
199 updateProgressJob.cancel(false);
203 public @Nullable BluetoothState findBluetoothState() {
204 return this.bluetoothState;
207 public @Nullable JsonPlaylists findPlaylists() {
208 return this.playLists;
211 public @Nullable JsonNotificationSound @Nullable [] findAlarmSounds() {
212 return this.alarmSounds;
215 public @Nullable List<JsonMusicProvider> findMusicProviders() {
216 return this.musicProviders;
219 private @Nullable Connection findConnection() {
220 AccountHandler accountHandler = this.account;
221 if (accountHandler != null) {
222 return accountHandler.findConnection();
227 public @Nullable AccountHandler findAccount() {
231 public @Nullable Device findDevice() {
235 public String findSerialNumber() {
236 String id = (String) getConfig().get(DEVICE_PROPERTY_SERIAL_NUMBER);
244 public void handleCommand(ChannelUID channelUID, Command command) {
246 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
247 int waitForUpdate = 1000;
248 boolean needBluetoothRefresh = false;
249 String lastKnownBluetoothMAC = this.lastKnownBluetoothMAC;
251 ScheduledFuture<?> updateStateJob = this.updateStateJob;
252 this.updateStateJob = null;
253 if (updateStateJob != null) {
254 this.disableUpdate = false;
255 updateStateJob.cancel(false);
257 AccountHandler account = this.account;
258 if (account == null) {
261 Connection connection = account.findConnection();
262 if (connection == null) {
265 Device device = this.device;
266 if (device == null) {
270 String channelId = channelUID.getId();
271 for (ChannelHandler channelHandler : channelHandlers) {
272 if (channelHandler.tryHandleCommand(device, connection, channelId, command)) {
278 if (channelId.equals(CHANNEL_PLAYER)) {
279 if (command == PlayPauseType.PAUSE || command == OnOffType.OFF) {
280 connection.command(device, "{\"type\":\"PauseCommand\"}");
281 } else if (command == PlayPauseType.PLAY || command == OnOffType.ON) {
283 connection.command(device, "{\"type\":\"PlayCommand\"}");
285 connection.playMusicVoiceCommand(device, this.musicProviderId, "!");
286 waitForUpdate = 3000;
288 } else if (command == NextPreviousType.NEXT) {
289 connection.command(device, "{\"type\":\"NextCommand\"}");
290 } else if (command == NextPreviousType.PREVIOUS) {
291 connection.command(device, "{\"type\":\"PreviousCommand\"}");
292 } else if (command == RewindFastforwardType.FASTFORWARD) {
293 connection.command(device, "{\"type\":\"ForwardCommand\"}");
294 } else if (command == RewindFastforwardType.REWIND) {
295 connection.command(device, "{\"type\":\"RewindCommand\"}");
298 // Notification commands
299 if (channelId.equals(CHANNEL_NOTIFICATION_VOLUME)) {
300 if (command instanceof PercentType) {
301 int volume = ((PercentType) command).intValue();
302 connection.notificationVolume(device, volume);
303 this.notificationVolumeLevel = volume;
305 account.forceCheckData();
308 if (channelId.equals(CHANNEL_ASCENDING_ALARM)) {
309 if (command == OnOffType.OFF) {
310 connection.ascendingAlarm(device, false);
311 this.ascendingAlarm = false;
313 account.forceCheckData();
315 if (command == OnOffType.ON) {
316 connection.ascendingAlarm(device, true);
317 this.ascendingAlarm = true;
319 account.forceCheckData();
322 // Media progress commands
323 Long mediaPosition = null;
324 if (channelId.equals(CHANNEL_MEDIA_PROGRESS)) {
325 if (command instanceof PercentType) {
326 PercentType value = (PercentType) command;
327 int percent = value.intValue();
328 mediaPosition = Math.round((mediaLengthMs / 1000d) * (percent / 100d));
331 if (channelId.equals(CHANNEL_MEDIA_PROGRESS_TIME)) {
332 if (command instanceof DecimalType) {
333 DecimalType value = (DecimalType) command;
334 mediaPosition = value.longValue();
336 if (command instanceof QuantityType<?>) {
337 QuantityType<?> value = (QuantityType<?>) command;
339 QuantityType<?> seconds = value.toUnit(SmartHomeUnits.SECOND);
340 if (seconds != null) {
341 mediaPosition = seconds.longValue();
345 if (mediaPosition != null) {
347 synchronized (progressLock) {
348 String seekCommand = "{\"type\":\"SeekCommand\",\"mediaPosition\":" + mediaPosition
349 + ",\"contentFocusClientId\":null}";
350 connection.command(device, seekCommand);
351 connection.command(device, seekCommand); // Must be sent twice, the first one is ignored sometimes
352 this.mediaProgressMs = mediaPosition * 1000;
353 mediaStartMs = System.currentTimeMillis() - this.mediaProgressMs;
354 updateMediaProgress(false);
358 if (channelId.equals(CHANNEL_VOLUME)) {
359 Integer volume = null;
360 if (command instanceof PercentType) {
361 PercentType value = (PercentType) command;
362 volume = value.intValue();
363 } else if (command == OnOffType.OFF) {
365 } else if (command == OnOffType.ON) {
366 volume = lastKnownVolume;
367 } else if (command == IncreaseDecreaseType.INCREASE) {
368 if (lastKnownVolume < 100) {
370 volume = lastKnownVolume;
372 } else if (command == IncreaseDecreaseType.DECREASE) {
373 if (lastKnownVolume > 0) {
375 volume = lastKnownVolume;
378 if (volume != null) {
379 if (StringUtils.equals(device.deviceFamily, "WHA")) {
380 connection.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + volume
381 + ",\"contentFocusClientId\":\"Default\"}");
383 connection.volume(device, volume);
385 lastKnownVolume = volume;
386 updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume));
390 // equalizer commands
391 if (channelId.equals(CHANNEL_EQUALIZER_BASS) || channelId.equals(CHANNEL_EQUALIZER_MIDRANGE)
392 || channelId.equals(CHANNEL_EQUALIZER_TREBLE)) {
393 if (handleEqualizerCommands(channelId, command, connection, device)) {
399 if (channelId.equals(CHANNEL_SHUFFLE)) {
400 if (command instanceof OnOffType) {
401 OnOffType value = (OnOffType) command;
403 connection.command(device, "{\"type\":\"ShuffleCommand\",\"shuffle\":\""
404 + (value == OnOffType.ON ? "true" : "false") + "\"}");
408 // play music command
409 if (channelId.equals(CHANNEL_MUSIC_PROVIDER_ID)) {
410 if (command instanceof StringType) {
412 String musicProviderId = ((StringType) command).toFullString();
413 if (!StringUtils.equals(musicProviderId, this.musicProviderId)) {
414 this.musicProviderId = musicProviderId;
415 if (this.isPlaying) {
416 connection.playMusicVoiceCommand(device, this.musicProviderId, "!");
417 waitForUpdate = 3000;
422 if (channelId.equals(CHANNEL_PLAY_MUSIC_VOICE_COMMAND)) {
423 if (command instanceof StringType) {
424 String voiceCommand = ((StringType) command).toFullString();
425 if (!this.musicProviderId.isEmpty()) {
426 connection.playMusicVoiceCommand(device, this.musicProviderId, voiceCommand);
427 waitForUpdate = 3000;
428 updatePlayMusicVoiceCommand = true;
433 // bluetooth commands
434 if (channelId.equals(CHANNEL_BLUETOOTH_MAC)) {
435 needBluetoothRefresh = true;
436 if (command instanceof StringType) {
437 String address = ((StringType) command).toFullString();
438 if (!address.isEmpty()) {
439 waitForUpdate = 4000;
441 connection.bluetooth(device, address);
444 if (channelId.equals(CHANNEL_BLUETOOTH)) {
445 needBluetoothRefresh = true;
446 if (command == OnOffType.ON) {
447 waitForUpdate = 4000;
448 String bluetoothId = lastKnownBluetoothMAC;
449 BluetoothState state = bluetoothState;
450 if (state != null && (StringUtils.isEmpty(bluetoothId))) {
451 PairedDevice[] pairedDeviceList = state.pairedDeviceList;
452 if (pairedDeviceList != null) {
453 for (PairedDevice paired : pairedDeviceList) {
454 if (paired == null) {
457 if (StringUtils.isNotEmpty(paired.address)) {
458 lastKnownBluetoothMAC = paired.address;
464 if (StringUtils.isNotEmpty(lastKnownBluetoothMAC)) {
465 connection.bluetooth(device, lastKnownBluetoothMAC);
467 } else if (command == OnOffType.OFF) {
468 connection.bluetooth(device, null);
471 if (channelId.equals(CHANNEL_BLUETOOTH_DEVICE_NAME)) {
472 needBluetoothRefresh = true;
474 // amazon music commands
475 if (channelId.equals(CHANNEL_AMAZON_MUSIC_TRACK_ID)) {
476 if (command instanceof StringType) {
477 String trackId = ((StringType) command).toFullString();
478 if (StringUtils.isNotEmpty(trackId)) {
479 waitForUpdate = 3000;
481 connection.playAmazonMusicTrack(device, trackId);
484 if (channelId.equals(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID)) {
485 if (command instanceof StringType) {
486 String playListId = ((StringType) command).toFullString();
487 if (StringUtils.isNotEmpty(playListId)) {
488 waitForUpdate = 3000;
490 connection.playAmazonMusicPlayList(device, playListId);
493 if (channelId.equals(CHANNEL_AMAZON_MUSIC)) {
494 if (command == OnOffType.ON) {
495 String lastKnownAmazonMusicId = this.lastKnownAmazonMusicId;
496 if (StringUtils.isNotEmpty(lastKnownAmazonMusicId)) {
497 waitForUpdate = 3000;
499 connection.playAmazonMusicTrack(device, lastKnownAmazonMusicId);
500 } else if (command == OnOffType.OFF) {
501 connection.playAmazonMusicTrack(device, "");
506 if (channelId.equals(CHANNEL_RADIO_STATION_ID)) {
507 if (command instanceof StringType) {
508 String stationId = ((StringType) command).toFullString();
509 if (StringUtils.isNotEmpty(stationId)) {
510 waitForUpdate = 3000;
512 connection.playRadio(device, stationId);
515 if (channelId.equals(CHANNEL_RADIO)) {
516 if (command == OnOffType.ON) {
517 String lastKnownRadioStationId = this.lastKnownRadioStationId;
518 if (StringUtils.isNotEmpty(lastKnownRadioStationId)) {
519 waitForUpdate = 3000;
521 connection.playRadio(device, lastKnownRadioStationId);
522 } else if (command == OnOffType.OFF) {
523 connection.playRadio(device, "");
528 if (channelId.equals(CHANNEL_REMIND)) {
529 if (command instanceof StringType) {
530 stopCurrentNotification();
531 String reminder = ((StringType) command).toFullString();
532 if (StringUtils.isNotEmpty(reminder)) {
533 waitForUpdate = 3000;
535 currentNotification = connection.notification(device, "Reminder", reminder, null);
536 currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> {
537 updateNotificationTimerState();
538 }, 1, 1, TimeUnit.SECONDS);
542 if (channelId.equals(CHANNEL_PLAY_ALARM_SOUND)) {
543 if (command instanceof StringType) {
544 stopCurrentNotification();
545 String alarmSound = ((StringType) command).toFullString();
546 if (StringUtils.isNotEmpty(alarmSound)) {
547 waitForUpdate = 3000;
549 String[] parts = alarmSound.split(":", 2);
550 JsonNotificationSound sound = new JsonNotificationSound();
551 if (parts.length == 2) {
552 sound.providerId = parts[0];
555 sound.providerId = "ECHO";
556 sound.id = alarmSound;
558 currentNotification = connection.notification(device, "Alarm", null, sound);
559 currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> {
560 updateNotificationTimerState();
561 }, 1, 1, TimeUnit.SECONDS);
567 if (channelId.equals(CHANNEL_TEXT_TO_SPEECH)) {
568 if (command instanceof StringType) {
569 String text = ((StringType) command).toFullString();
570 if (StringUtils.isNotEmpty(text)) {
571 waitForUpdate = 1000;
572 updateTextToSpeech = true;
573 startTextToSpeech(connection, device, text);
577 if (channelId.equals(CHANNEL_TEXT_TO_SPEECH_VOLUME)) {
578 if (command instanceof PercentType) {
579 PercentType value = (PercentType) command;
580 textToSpeechVolume = value.intValue();
581 } else if (command == OnOffType.OFF) {
582 textToSpeechVolume = 0;
583 } else if (command == OnOffType.ON) {
584 textToSpeechVolume = lastKnownVolume;
585 } else if (command == IncreaseDecreaseType.INCREASE) {
586 if (textToSpeechVolume < 100) {
587 textToSpeechVolume++;
589 } else if (command == IncreaseDecreaseType.DECREASE) {
590 if (textToSpeechVolume > 0) {
591 textToSpeechVolume--;
594 this.updateState(channelId, new PercentType(textToSpeechVolume));
596 if (channelId.equals(CHANNEL_LAST_VOICE_COMMAND)) {
597 if (command instanceof StringType) {
598 String text = ((StringType) command).toFullString();
599 if (StringUtils.isNotEmpty(text)) {
601 startTextToSpeech(connection, device, text);
605 if (channelId.equals(CHANNEL_START_COMMAND)) {
606 if (command instanceof StringType) {
607 String commandText = ((StringType) command).toFullString();
608 if (StringUtils.isNotEmpty(commandText)) {
609 updateStartCommand = true;
610 if (commandText.startsWith(FLASH_BRIEFING_COMMAND_PREFIX)) {
611 // Handle custom flashbriefings commands
612 String flashbriefing = commandText.substring(FLASH_BRIEFING_COMMAND_PREFIX.length());
614 for (FlashBriefingProfileHandler flashBriefing : account
615 .getFlashBriefingProfileHandlers()) {
616 ThingUID flashBriefingId = flashBriefing.getThing().getUID();
617 if (StringUtils.equals(flashBriefing.getThing().getUID().getId(), flashbriefing)) {
618 flashBriefing.handleCommand(new ChannelUID(flashBriefingId, CHANNEL_PLAY_ON_DEVICE),
619 new StringType(device.serialNumber));
624 // Handle standard commands
625 if (!commandText.startsWith("Alexa.")) {
626 commandText = "Alexa." + commandText + ".Play";
628 waitForUpdate = 1000;
629 connection.executeSequenceCommand(device, commandText, null);
634 if (channelId.equals(CHANNEL_START_ROUTINE)) {
635 if (command instanceof StringType) {
636 String utterance = ((StringType) command).toFullString();
637 if (StringUtils.isNotEmpty(utterance)) {
638 waitForUpdate = 1000;
639 updateRoutine = true;
640 connection.startRoutine(device, utterance);
644 if (waitForUpdate < 0) {
647 // force update of the state
648 this.disableUpdate = true;
649 final boolean bluetoothRefresh = needBluetoothRefresh;
650 Runnable doRefresh = () -> {
651 this.disableUpdate = false;
652 BluetoothState state = null;
653 if (bluetoothRefresh) {
654 JsonBluetoothStates states;
655 states = connection.getBluetoothConnectionStates();
656 if (states != null) {
657 state = states.findStateByDevice(device);
661 updateState(account, device, state, null, null, null, null, null);
663 if (command instanceof RefreshType) {
665 account.forceCheckData();
667 if (waitForUpdate == 0) {
670 this.updateStateJob = scheduler.schedule(doRefresh, waitForUpdate, TimeUnit.MILLISECONDS);
672 } catch (IOException | URISyntaxException e) {
673 logger.info("handleCommand fails", e);
677 private boolean handleEqualizerCommands(String channelId, Command command, Connection connection, Device device)
678 throws URISyntaxException {
679 if (command instanceof RefreshType) {
680 this.lastKnownEqualizer = null;
682 if (command instanceof DecimalType) {
683 DecimalType value = (DecimalType) command;
684 if (this.lastKnownEqualizer == null) {
685 updateEqualizerState();
687 JsonEqualizer lastKnownEqualizer = this.lastKnownEqualizer;
688 if (lastKnownEqualizer != null) {
689 JsonEqualizer newEqualizerSetting = lastKnownEqualizer.createClone();
690 if (channelId.equals(CHANNEL_EQUALIZER_BASS)) {
691 newEqualizerSetting.bass = value.intValue();
693 if (channelId.equals(CHANNEL_EQUALIZER_MIDRANGE)) {
694 newEqualizerSetting.mid = value.intValue();
696 if (channelId.equals(CHANNEL_EQUALIZER_TREBLE)) {
697 newEqualizerSetting.treble = value.intValue();
700 connection.setEqualizer(device, newEqualizerSetting);
702 } catch (HttpException | IOException | ConnectionException e) {
703 logger.debug("Update equalizer failed", e);
704 this.lastKnownEqualizer = null;
711 private void startTextToSpeech(Connection connection, Device device, String text)
712 throws IOException, URISyntaxException {
713 Integer volume = null;
714 if (textToSpeechVolume != 0) {
715 volume = textToSpeechVolume;
717 connection.textToSpeech(device, text, volume, lastKnownVolume);
721 public void startAnnouncment(Device device, String speak, String bodyText, @Nullable String title,
722 @Nullable Integer volume) throws IOException, URISyntaxException {
723 Connection connection = this.findConnection();
724 if (connection == null) {
727 if (volume == null && textToSpeechVolume != 0) {
728 volume = textToSpeechVolume;
730 if (volume != null && volume < 0) {
731 volume = null; // the meaning of negative values is 'do not use'. The api requires null in this case.
733 connection.announcement(device, speak, bodyText, title, volume, lastKnownVolume);
736 private void stopCurrentNotification() {
737 ScheduledFuture<?> currentNotifcationUpdateTimer = this.currentNotifcationUpdateTimer;
738 if (currentNotifcationUpdateTimer != null) {
739 this.currentNotifcationUpdateTimer = null;
740 currentNotifcationUpdateTimer.cancel(true);
742 JsonNotificationResponse currentNotification = this.currentNotification;
743 if (currentNotification != null) {
744 this.currentNotification = null;
745 Connection currentConnection = this.findConnection();
746 if (currentConnection != null) {
748 currentConnection.stopNotification(currentNotification);
749 } catch (IOException | URISyntaxException e) {
750 logger.warn("Stop notification failed", e);
756 private void updateNotificationTimerState() {
757 boolean stopCurrentNotification = true;
758 JsonNotificationResponse currentNotification = this.currentNotification;
760 if (currentNotification != null) {
761 Connection currentConnection = this.findConnection();
762 if (currentConnection != null) {
763 JsonNotificationResponse newState = currentConnection.getNotificationState(currentNotification);
764 if (newState != null && "ON".equals(newState.status)) {
765 stopCurrentNotification = false;
769 } catch (IOException | URISyntaxException e) {
770 logger.warn("update notification state fails", e);
772 if (stopCurrentNotification) {
773 if (currentNotification != null) {
774 String type = currentNotification.type;
776 if (type.equals("Reminder")) {
777 updateState(CHANNEL_REMIND, new StringType(""));
778 updateRemind = false;
780 if (type.equals("Alarm")) {
781 updateState(CHANNEL_PLAY_ALARM_SOUND, new StringType(""));
786 stopCurrentNotification();
790 public void updateState(AccountHandler accountHandler, @Nullable Device device,
791 @Nullable BluetoothState bluetoothState, @Nullable DeviceNotificationState deviceNotificationState,
792 @Nullable AscendingAlarmModel ascendingAlarmModel, @Nullable JsonPlaylists playlists,
793 @Nullable JsonNotificationSound @Nullable [] alarmSounds,
794 @Nullable List<JsonMusicProvider> musicProviders) {
796 this.logger.debug("Handle updateState {}", this.getThing().getUID());
798 if (deviceNotificationState != null) {
799 notificationVolumeLevel = deviceNotificationState.volumeLevel;
801 if (ascendingAlarmModel != null) {
802 ascendingAlarm = ascendingAlarmModel.ascendingAlarmEnabled;
804 if (playlists != null) {
805 this.playLists = playlists;
807 if (alarmSounds != null) {
808 this.alarmSounds = alarmSounds;
810 if (musicProviders != null) {
811 this.musicProviders = musicProviders;
813 if (!setDeviceAndUpdateThingState(accountHandler, device, null)) {
814 this.logger.debug("Handle updateState {} aborted: Not online", this.getThing().getUID());
817 if (device == null) {
818 this.logger.debug("Handle updateState {} aborted: No device", this.getThing().getUID());
822 if (this.disableUpdate) {
823 this.logger.debug("Handle updateState {} aborted: Disabled", this.getThing().getUID());
826 Connection connection = this.findConnection();
827 if (connection == null) {
831 if (this.lastKnownEqualizer == null) {
832 updateEqualizerState();
835 PlayerInfo playerInfo = null;
836 Provider provider = null;
837 InfoText infoText = null;
838 MainArt mainArt = null;
839 String musicProviderId = null;
840 Progress progress = null;
842 JsonPlayerState playerState = connection.getPlayer(device);
843 if (playerState != null) {
844 playerInfo = playerState.playerInfo;
845 if (playerInfo != null) {
846 infoText = playerInfo.infoText;
847 if (infoText == null) {
848 infoText = playerInfo.miniInfoText;
850 mainArt = playerInfo.mainArt;
851 provider = playerInfo.provider;
852 if (provider != null) {
853 musicProviderId = provider.providerName;
854 // Map the music provider id to the one used for starting music with voice command
855 if (musicProviderId != null) {
856 musicProviderId = musicProviderId.toUpperCase();
858 if (StringUtils.equals(musicProviderId, "AMAZON MUSIC")) {
859 musicProviderId = "AMAZON_MUSIC";
861 if (StringUtils.equals(musicProviderId, "CLOUD_PLAYER")) {
862 musicProviderId = "AMAZON_MUSIC";
864 if (StringUtils.startsWith(musicProviderId, "TUNEIN")) {
865 musicProviderId = "TUNEIN";
867 if (StringUtils.startsWithIgnoreCase(musicProviderId, "iHeartRadio")) {
868 musicProviderId = "I_HEART_RADIO";
870 if (StringUtils.containsIgnoreCase(musicProviderId, "Apple")
871 && StringUtils.containsIgnoreCase(musicProviderId, "Music")) {
872 musicProviderId = "APPLE_MUSIC";
876 progress = playerInfo.progress;
879 } catch (HttpException e) {
880 if (e.getCode() != 400) {
881 logger.info("getPlayer fails", e);
883 } catch (IOException | URISyntaxException e) {
884 logger.info("getPlayer fails", e);
887 isPlaying = (playerInfo != null && StringUtils.equals(playerInfo.state, "PLAYING"));
889 isPaused = (playerInfo != null && StringUtils.equals(playerInfo.state, "PAUSED"));
890 synchronized (progressLock) {
891 Boolean showTime = null;
892 Long mediaLength = null;
893 Long mediaProgress = null;
894 if (progress != null) {
895 showTime = progress.showTiming;
896 mediaLength = progress.mediaLength;
897 mediaProgress = progress.mediaProgress;
899 if (showTime != null && showTime && mediaProgress != null && mediaLength != null) {
900 mediaProgressMs = mediaProgress * 1000;
901 mediaLengthMs = mediaLength * 1000;
902 mediaStartMs = System.currentTimeMillis() - mediaProgressMs;
904 if (updateProgressJob == null) {
905 updateProgressJob = scheduler.scheduleWithFixedDelay(this::updateMediaProgress, 1000, 1000,
906 TimeUnit.MILLISECONDS);
917 updateMediaProgress(true);
920 JsonMediaState mediaState = null;
922 if (StringUtils.equalsIgnoreCase(musicProviderId, "AMAZON_MUSIC")
923 || StringUtils.equalsIgnoreCase(musicProviderId, "TUNEIN")) {
924 mediaState = connection.getMediaState(device);
926 } catch (HttpException e) {
927 if (e.getCode() == 400) {
928 updateState(CHANNEL_RADIO_STATION_ID, new StringType(""));
930 logger.info("getMediaState fails", e);
932 } catch (IOException | URISyntaxException 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 && isPlaying && StringUtils.equals(mediaState.providerId, "CLOUD_PLAYER")
948 && StringUtils.isNotEmpty(mediaState.contentId)) {
949 amazonMusicTrackId = mediaState.contentId;
950 lastKnownAmazonMusicId = amazonMusicTrackId;
955 String bluetoothMAC = "";
956 String bluetoothDeviceName = "";
957 boolean bluetoothIsConnected = false;
958 if (bluetoothState != null) {
959 this.bluetoothState = bluetoothState;
960 PairedDevice[] pairedDeviceList = bluetoothState.pairedDeviceList;
961 if (pairedDeviceList != null) {
962 for (PairedDevice paired : pairedDeviceList) {
963 if (paired == null) {
966 if (paired.connected && paired.address != null) {
967 bluetoothIsConnected = true;
968 bluetoothMAC = paired.address;
969 bluetoothDeviceName = paired.friendlyName;
970 if (StringUtils.isEmpty(bluetoothDeviceName)) {
971 bluetoothDeviceName = paired.address;
978 if (StringUtils.isNotEmpty(bluetoothMAC)) {
979 lastKnownBluetoothMAC = bluetoothMAC;
983 boolean isRadio = false;
984 if (mediaState != null && StringUtils.isNotEmpty(mediaState.radioStationId)) {
985 lastKnownRadioStationId = mediaState.radioStationId;
986 if (StringUtils.equalsIgnoreCase(musicProviderId, "TUNEIN")) {
990 String radioStationId = "";
991 if (isRadio && mediaState != null && StringUtils.equals(mediaState.currentState, "PLAYING")
992 && mediaState.radioStationId != null) {
993 radioStationId = mediaState.radioStationId;
996 // handle title, subtitle, imageUrl
998 String subTitle1 = "";
999 String subTitle2 = "";
1000 String imageUrl = "";
1001 if (infoText != null) {
1002 if (infoText.title != null) {
1003 title = infoText.title;
1005 if (infoText.subText1 != null) {
1006 subTitle1 = infoText.subText1;
1009 if (infoText.subText2 != null) {
1010 subTitle2 = infoText.subText2;
1013 if (mainArt != null) {
1014 if (mainArt.url != null) {
1015 imageUrl = mainArt.url;
1018 if (mediaState != null) {
1019 QueueEntry[] queueEntries = mediaState.queue;
1020 if (queueEntries != null && queueEntries.length > 0) {
1021 QueueEntry entry = queueEntries[0];
1022 if (entry != null) {
1024 if (StringUtils.isEmpty(imageUrl) && entry.imageURL != null) {
1025 imageUrl = entry.imageURL;
1027 if (StringUtils.isEmpty(subTitle1) && entry.radioStationSlogan != null) {
1028 subTitle1 = entry.radioStationSlogan;
1030 if (StringUtils.isEmpty(subTitle2) && entry.radioStationLocation != null) {
1031 subTitle2 = entry.radioStationLocation;
1039 String providerDisplayName = "";
1040 if (provider != null) {
1041 if (provider.providerDisplayName != null) {
1042 providerDisplayName = provider.providerDisplayName;
1044 if (StringUtils.isNotEmpty(provider.providerName) && StringUtils.isEmpty(providerDisplayName)) {
1045 providerDisplayName = provider.providerName;
1050 Integer volume = null;
1051 if (!connection.isSequenceNodeQueueRunning()) {
1052 if (mediaState != null) {
1053 volume = mediaState.volume;
1055 if (playerInfo != null && volume == null) {
1056 Volume volumnInfo = playerInfo.volume;
1057 if (volumnInfo != null) {
1058 volume = volumnInfo.volume;
1061 if (volume != null && volume > 0) {
1062 lastKnownVolume = volume;
1064 if (volume == null) {
1065 volume = lastKnownVolume;
1069 if (updateRemind && currentNotifcationUpdateTimer == null) {
1070 updateRemind = false;
1071 updateState(CHANNEL_REMIND, new StringType(""));
1073 if (updateAlarm && currentNotifcationUpdateTimer == null) {
1074 updateAlarm = false;
1075 updateState(CHANNEL_PLAY_ALARM_SOUND, new StringType(""));
1077 if (updateRoutine) {
1078 updateRoutine = false;
1079 updateState(CHANNEL_START_ROUTINE, new StringType(""));
1081 if (updateTextToSpeech) {
1082 updateTextToSpeech = false;
1083 updateState(CHANNEL_TEXT_TO_SPEECH, new StringType(""));
1085 if (updatePlayMusicVoiceCommand) {
1086 updatePlayMusicVoiceCommand = false;
1087 updateState(CHANNEL_PLAY_MUSIC_VOICE_COMMAND, new StringType(""));
1089 if (updateStartCommand) {
1090 updateStartCommand = false;
1091 updateState(CHANNEL_START_COMMAND, new StringType(""));
1094 updateState(CHANNEL_MUSIC_PROVIDER_ID, new StringType(musicProviderId));
1095 updateState(CHANNEL_AMAZON_MUSIC_TRACK_ID, new StringType(amazonMusicTrackId));
1096 updateState(CHANNEL_AMAZON_MUSIC, isPlaying && amazonMusic ? OnOffType.ON : OnOffType.OFF);
1097 updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID, new StringType(amazonMusicPlayListId));
1098 updateState(CHANNEL_RADIO_STATION_ID, new StringType(radioStationId));
1099 updateState(CHANNEL_RADIO, isPlaying && isRadio ? OnOffType.ON : OnOffType.OFF);
1100 updateState(CHANNEL_PROVIDER_DISPLAY_NAME, new StringType(providerDisplayName));
1101 updateState(CHANNEL_PLAYER, isPlaying ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
1102 updateState(CHANNEL_IMAGE_URL, new StringType(imageUrl));
1103 updateState(CHANNEL_TITLE, new StringType(title));
1104 if (volume != null) {
1105 updateState(CHANNEL_VOLUME, new PercentType(volume));
1107 updateState(CHANNEL_SUBTITLE1, new StringType(subTitle1));
1108 updateState(CHANNEL_SUBTITLE2, new StringType(subTitle2));
1109 if (bluetoothState != null) {
1110 updateState(CHANNEL_BLUETOOTH, bluetoothIsConnected ? OnOffType.ON : OnOffType.OFF);
1111 updateState(CHANNEL_BLUETOOTH_MAC, new StringType(bluetoothMAC));
1112 updateState(CHANNEL_BLUETOOTH_DEVICE_NAME, new StringType(bluetoothDeviceName));
1115 updateState(CHANNEL_ASCENDING_ALARM,
1116 ascendingAlarm != null ? (ascendingAlarm ? OnOffType.ON : OnOffType.OFF) : UnDefType.UNDEF);
1118 final Integer notificationVolumeLevel = this.notificationVolumeLevel;
1119 if (notificationVolumeLevel != null) {
1120 updateState(CHANNEL_NOTIFICATION_VOLUME, new PercentType(notificationVolumeLevel));
1122 updateState(CHANNEL_NOTIFICATION_VOLUME, UnDefType.UNDEF);
1124 } catch (Exception e) {
1125 this.logger.debug("Handle updateState {} failed: {}", this.getThing().getUID(), e.getMessage(), e);
1127 disableUpdate = false;
1128 throw e; // Rethrow same exception
1132 private void updateEqualizerState() {
1133 if (!this.capabilities.contains("SOUND_SETTINGS")) {
1137 Connection connection = findConnection();
1138 if (connection == null) {
1141 Device device = findDevice();
1142 if (device == null) {
1145 Integer bass = null;
1146 Integer midrange = null;
1147 Integer treble = null;
1149 JsonEqualizer equalizer = connection.getEqualizer(device);
1150 if (equalizer != null) {
1151 bass = equalizer.bass;
1152 midrange = equalizer.mid;
1153 treble = equalizer.treble;
1155 this.lastKnownEqualizer = equalizer;
1156 } catch (IOException | URISyntaxException | HttpException | ConnectionException e) {
1157 logger.debug("Get equalizer failes", e);
1161 updateState(CHANNEL_EQUALIZER_BASS, new DecimalType(bass));
1163 if (midrange != null) {
1164 updateState(CHANNEL_EQUALIZER_MIDRANGE, new DecimalType(midrange));
1166 if (treble != null) {
1167 updateState(CHANNEL_EQUALIZER_TREBLE, new DecimalType(treble));
1171 private void updateMediaProgress() {
1172 updateMediaProgress(false);
1175 private void updateMediaProgress(boolean updateMediaLength) {
1176 synchronized (progressLock) {
1177 if (mediaStartMs > 0) {
1178 long currentPlayTimeMs = isPlaying ? System.currentTimeMillis() - mediaStartMs : mediaProgressMs;
1179 if (mediaLengthMs > 0) {
1180 int progressPercent = (int) Math.min(100,
1181 Math.round((double) currentPlayTimeMs / (double) mediaLengthMs * 100));
1182 updateState(CHANNEL_MEDIA_PROGRESS, new PercentType(progressPercent));
1184 updateState(CHANNEL_MEDIA_PROGRESS, UnDefType.UNDEF);
1186 updateState(CHANNEL_MEDIA_PROGRESS_TIME,
1187 new QuantityType<>(currentPlayTimeMs / 1000, SmartHomeUnits.SECOND));
1188 if (updateMediaLength) {
1189 updateState(CHANNEL_MEDIA_LENGTH, new QuantityType<>(mediaLengthMs / 1000, SmartHomeUnits.SECOND));
1192 updateState(CHANNEL_MEDIA_PROGRESS, UnDefType.UNDEF);
1193 updateState(CHANNEL_MEDIA_LENGTH, UnDefType.UNDEF);
1194 updateState(CHANNEL_MEDIA_PROGRESS_TIME, UnDefType.UNDEF);
1195 if (updateMediaLength) {
1196 updateState(CHANNEL_MEDIA_LENGTH, UnDefType.UNDEF);
1202 public void handlePushActivity(Activity pushActivity) {
1203 if ("DISCARDED_NON_DEVICE_DIRECTED_INTENT".equals(pushActivity.activityStatus)) {
1206 Description description = pushActivity.parseDescription();
1207 if (StringUtils.isEmpty(description.firstUtteranceId)
1208 || StringUtils.startsWithIgnoreCase(description.firstUtteranceId, "TextClient:")) {
1211 if (StringUtils.isEmpty(description.firstStreamId)) {
1214 String spokenText = description.summary;
1215 if (spokenText != null && StringUtils.isNotEmpty(spokenText)) {
1217 String wakeWordPrefix = this.wakeWord;
1218 if (wakeWordPrefix != null) {
1219 wakeWordPrefix += " ";
1220 if (StringUtils.startsWithIgnoreCase(spokenText, wakeWordPrefix)) {
1221 spokenText = spokenText.substring(wakeWordPrefix.length());
1225 if (lastSpokenText.isEmpty() || lastSpokenText.equals(spokenText)) {
1226 updateState(CHANNEL_LAST_VOICE_COMMAND, new StringType(""));
1228 lastSpokenText = spokenText;
1229 updateState(CHANNEL_LAST_VOICE_COMMAND, new StringType(spokenText));
1233 public void handlePushCommand(String command, String payload) {
1234 this.logger.debug("Handle push command {}", command);
1236 case "PUSH_VOLUME_CHANGE":
1237 JsonCommandPayloadPushVolumeChange volumeChange = gson.fromJson(payload,
1238 JsonCommandPayloadPushVolumeChange.class);
1239 Connection connection = this.findConnection();
1241 Integer volumeSetting = volumeChange.volumeSetting;
1243 Boolean muted = volumeChange.isMuted;
1244 if (muted != null && muted) {
1245 updateState(CHANNEL_VOLUME, new PercentType(0));
1247 if (volumeSetting != null && connection != null && !connection.isSequenceNodeQueueRunning()) {
1248 lastKnownVolume = volumeSetting;
1249 updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume));
1252 case "PUSH_EQUALIZER_STATE_CHANGE":
1253 updateEqualizerState();
1256 AccountHandler account = this.account;
1257 Device device = this.device;
1258 if (account != null && device != null) {
1259 this.disableUpdate = false;
1260 updateState(account, device, null, null, null, null, null, null);
1265 public void updateNotifications(ZonedDateTime currentTime, ZonedDateTime now,
1266 @Nullable JsonCommandPayloadPushNotificationChange pushPayload, JsonNotificationResponse[] notifications) {
1267 Device device = this.device;
1268 if (device == null) {
1272 ZonedDateTime nextReminder = null;
1273 ZonedDateTime nextAlarm = null;
1274 ZonedDateTime nextMusicAlarm = null;
1275 ZonedDateTime nextTimer = null;
1276 for (JsonNotificationResponse notification : notifications) {
1277 if (StringUtils.equals(notification.deviceSerialNumber, device.serialNumber)) {
1278 // notification for this device
1279 if (StringUtils.equals(notification.status, "ON")) {
1280 if ("Reminder".equals(notification.type)) {
1281 String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1282 ZonedDateTime alarmTime = ZonedDateTime
1283 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1284 if (StringUtils.isNotBlank(notification.recurringPattern) && alarmTime.isBefore(now)) {
1285 continue; // Ignore recurring entry if alarm time is before now
1287 if (nextReminder == null || alarmTime.isBefore(nextReminder)) {
1288 nextReminder = alarmTime;
1290 } else if ("Timer".equals(notification.type)) {
1291 // use remaining time
1292 ZonedDateTime alarmTime = currentTime.plus(notification.remainingTime, ChronoUnit.MILLIS);
1293 if (nextTimer == null || alarmTime.isBefore(nextTimer)) {
1294 nextTimer = alarmTime;
1296 } else if ("Alarm".equals(notification.type)) {
1297 String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1298 ZonedDateTime alarmTime = ZonedDateTime
1299 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1300 if (StringUtils.isNotBlank(notification.recurringPattern) && alarmTime.isBefore(now)) {
1301 continue; // Ignore recurring entry if alarm time is before now
1303 if (nextAlarm == null || alarmTime.isBefore(nextAlarm)) {
1304 nextAlarm = alarmTime;
1306 } else if ("MusicAlarm".equals(notification.type)) {
1307 String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
1308 ZonedDateTime alarmTime = ZonedDateTime
1309 .parse(notification.originalDate + "T" + notification.originalTime + offset);
1310 if (StringUtils.isNotBlank(notification.recurringPattern) && alarmTime.isBefore(now)) {
1311 continue; // Ignore recurring entry if alarm time is before now
1313 if (nextMusicAlarm == null || alarmTime.isBefore(nextMusicAlarm)) {
1314 nextMusicAlarm = alarmTime;
1321 updateState(CHANNEL_NEXT_REMINDER, nextReminder == null ? UnDefType.UNDEF : new DateTimeType(nextReminder));
1322 updateState(CHANNEL_NEXT_ALARM, nextAlarm == null ? UnDefType.UNDEF : new DateTimeType(nextAlarm));
1323 updateState(CHANNEL_NEXT_MUSIC_ALARM,
1324 nextMusicAlarm == null ? UnDefType.UNDEF : new DateTimeType(nextMusicAlarm));
1325 updateState(CHANNEL_NEXT_TIMER, nextTimer == null ? UnDefType.UNDEF : new DateTimeType(nextTimer));
1329 public void updateChannelState(String channelId, State state) {
1330 updateState(channelId, state);