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 java.io.IOException;
16 import java.net.URISyntaxException;
17 import java.net.URLEncoder;
18 import java.net.UnknownHostException;
19 import java.time.ZonedDateTime;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.List;
27 import java.util.Objects;
29 import java.util.concurrent.CopyOnWriteArraySet;
30 import java.util.concurrent.LinkedBlockingQueue;
31 import java.util.concurrent.ScheduledFuture;
32 import java.util.concurrent.TimeUnit;
33 import java.util.stream.Collectors;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.openhab.binding.amazonechocontrol.internal.AccountHandlerConfig;
38 import org.openhab.binding.amazonechocontrol.internal.AccountServlet;
39 import org.openhab.binding.amazonechocontrol.internal.Connection;
40 import org.openhab.binding.amazonechocontrol.internal.ConnectionException;
41 import org.openhab.binding.amazonechocontrol.internal.HttpException;
42 import org.openhab.binding.amazonechocontrol.internal.IWebSocketCommandHandler;
43 import org.openhab.binding.amazonechocontrol.internal.WebSocketConnection;
44 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandler;
45 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandlerSendMessage;
46 import org.openhab.binding.amazonechocontrol.internal.channelhandler.IAmazonThingHandler;
47 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity.SourceDeviceId;
48 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm.AscendingAlarmModel;
49 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates;
50 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState;
51 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushActivity;
52 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushActivity.Key;
53 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushDevice;
54 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushDevice.DopplerId;
55 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushNotificationChange;
56 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState.DeviceNotificationState;
57 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device;
58 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed;
59 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider;
60 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse;
61 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound;
62 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists;
63 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPushCommand;
64 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice;
65 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonWakeWords.WakeWord;
66 import org.openhab.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice;
67 import org.openhab.binding.amazonechocontrol.internal.smarthome.SmartHomeDeviceStateGroupUpdateCalculator;
68 import org.openhab.core.storage.Storage;
69 import org.openhab.core.thing.Bridge;
70 import org.openhab.core.thing.ChannelUID;
71 import org.openhab.core.thing.Thing;
72 import org.openhab.core.thing.ThingStatus;
73 import org.openhab.core.thing.ThingStatusDetail;
74 import org.openhab.core.thing.ThingUID;
75 import org.openhab.core.thing.binding.BaseBridgeHandler;
76 import org.openhab.core.thing.binding.ThingHandler;
77 import org.openhab.core.types.Command;
78 import org.openhab.core.types.RefreshType;
79 import org.openhab.core.types.State;
80 import org.osgi.service.http.HttpService;
81 import org.slf4j.Logger;
82 import org.slf4j.LoggerFactory;
84 import com.google.gson.Gson;
85 import com.google.gson.JsonArray;
86 import com.google.gson.JsonSyntaxException;
89 * Handles the connection to the amazon server.
91 * @author Michael Geramb - Initial Contribution
94 public class AccountHandler extends BaseBridgeHandler implements IWebSocketCommandHandler, IAmazonThingHandler {
95 private final Logger logger = LoggerFactory.getLogger(AccountHandler.class);
96 private final Storage<String> stateStorage;
97 private @Nullable Connection connection;
98 private @Nullable WebSocketConnection webSocketConnection;
100 private final Set<EchoHandler> echoHandlers = new CopyOnWriteArraySet<>();
101 private final Set<SmartHomeDeviceHandler> smartHomeDeviceHandlers = new CopyOnWriteArraySet<>();
102 private final Set<FlashBriefingProfileHandler> flashBriefingProfileHandlers = new CopyOnWriteArraySet<>();
104 private final Object synchronizeConnection = new Object();
105 private Map<String, Device> jsonSerialNumberDeviceMapping = new HashMap<>();
106 private Map<String, SmartHomeBaseDevice> jsonIdSmartHomeDeviceMapping = new HashMap<>();
108 private @Nullable ScheduledFuture<?> checkDataJob;
109 private @Nullable ScheduledFuture<?> checkLoginJob;
110 private @Nullable ScheduledFuture<?> updateSmartHomeStateJob;
111 private @Nullable ScheduledFuture<?> refreshAfterCommandJob;
112 private @Nullable ScheduledFuture<?> refreshSmartHomeAfterCommandJob;
113 private final Object synchronizeSmartHomeJobScheduler = new Object();
114 private @Nullable ScheduledFuture<?> forceCheckDataJob;
115 private String currentFlashBriefingJson = "";
116 private final HttpService httpService;
117 private @Nullable AccountServlet accountServlet;
118 private final Gson gson;
119 private int checkDataCounter;
120 private final LinkedBlockingQueue<String> requestedDeviceUpdates = new LinkedBlockingQueue<>();
121 private @Nullable SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator;
122 private List<ChannelHandler> channelHandlers = new ArrayList<>();
124 private AccountHandlerConfig handlerConfig = new AccountHandlerConfig();
126 public AccountHandler(Bridge bridge, HttpService httpService, Storage<String> stateStorage, Gson gson) {
129 this.httpService = httpService;
130 this.stateStorage = stateStorage;
131 channelHandlers.add(new ChannelHandlerSendMessage(this, this.gson));
135 public void initialize() {
136 handlerConfig = getConfig().as(AccountHandlerConfig.class);
138 synchronized (synchronizeConnection) {
139 Connection connection = this.connection;
140 if (connection == null) {
141 this.connection = new Connection(null, gson);
145 if (accountServlet == null) {
147 accountServlet = new AccountServlet(httpService, this.getThing().getUID().getId(), this, gson);
148 } catch (IllegalStateException e) {
149 logger.warn("Failed to create account servlet", e);
153 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login");
155 checkLoginJob = scheduler.scheduleWithFixedDelay(this::checkLogin, 0, 60, TimeUnit.SECONDS);
156 checkDataJob = scheduler.scheduleWithFixedDelay(this::checkData, 4, 60, TimeUnit.SECONDS);
158 int pollingIntervalAlexa = handlerConfig.pollingIntervalSmartHomeAlexa;
159 if (pollingIntervalAlexa < 10) {
160 pollingIntervalAlexa = 10;
162 int pollingIntervalSkills = handlerConfig.pollingIntervalSmartSkills;
163 if (pollingIntervalSkills < 60) {
164 pollingIntervalSkills = 60;
166 smartHomeDeviceStateGroupUpdateCalculator = new SmartHomeDeviceStateGroupUpdateCalculator(pollingIntervalAlexa,
167 pollingIntervalSkills);
168 updateSmartHomeStateJob = scheduler.scheduleWithFixedDelay(() -> updateSmartHomeState(null), 20, 10,
173 public void updateChannelState(String channelId, State state) {
174 updateState(channelId, state);
178 public void handleCommand(ChannelUID channelUID, Command command) {
180 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
181 Connection connection = this.connection;
182 if (connection == null) {
186 String channelId = channelUID.getId();
187 for (ChannelHandler channelHandler : channelHandlers) {
188 if (channelHandler.tryHandleCommand(new Device(), connection, channelId, command)) {
192 if (command instanceof RefreshType) {
195 } catch (IOException | URISyntaxException | InterruptedException e) {
196 logger.info("handleCommand fails", e);
201 public void startAnnouncement(Device device, String speak, String bodyText, @Nullable String title,
202 @Nullable Integer volume) throws IOException, URISyntaxException {
203 EchoHandler echoHandler = findEchoHandlerBySerialNumber(device.serialNumber);
204 if (echoHandler != null) {
205 echoHandler.startAnnouncement(device, speak, bodyText, title, volume);
209 public List<FlashBriefingProfileHandler> getFlashBriefingProfileHandlers() {
210 return new ArrayList<>(flashBriefingProfileHandlers);
213 public List<Device> getLastKnownDevices() {
214 return new ArrayList<>(jsonSerialNumberDeviceMapping.values());
217 public List<SmartHomeBaseDevice> getLastKnownSmartHomeDevices() {
218 return new ArrayList<>(jsonIdSmartHomeDeviceMapping.values());
221 public void addEchoHandler(EchoHandler echoHandler) {
222 if (echoHandlers.add(echoHandler)) {
228 public void addSmartHomeDeviceHandler(SmartHomeDeviceHandler smartHomeDeviceHandler) {
229 if (smartHomeDeviceHandlers.add(smartHomeDeviceHandler)) {
234 public void forceCheckData() {
235 if (forceCheckDataJob == null) {
236 forceCheckDataJob = scheduler.schedule(this::checkData, 1000, TimeUnit.MILLISECONDS);
240 public @Nullable Thing findThingBySerialNumber(@Nullable String deviceSerialNumber) {
241 EchoHandler echoHandler = findEchoHandlerBySerialNumber(deviceSerialNumber);
242 if (echoHandler != null) {
243 return echoHandler.getThing();
248 public @Nullable EchoHandler findEchoHandlerBySerialNumber(@Nullable String deviceSerialNumber) {
249 for (EchoHandler echoHandler : echoHandlers) {
250 if (deviceSerialNumber != null && deviceSerialNumber.equals(echoHandler.findSerialNumber())) {
257 public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBriefingProfileHandler) {
258 flashBriefingProfileHandlers.add(flashBriefingProfileHandler);
259 Connection connection = this.connection;
260 if (connection != null && connection.getIsLoggedIn()) {
261 if (currentFlashBriefingJson.isEmpty()) {
262 updateFlashBriefingProfiles(connection);
264 flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson);
268 private void scheduleUpdate() {
269 checkDataCounter = 999;
273 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
274 super.childHandlerInitialized(childHandler, childThing);
279 public void handleRemoval() {
281 super.handleRemoval();
285 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
286 // check for echo handler
287 if (childHandler instanceof EchoHandler) {
288 echoHandlers.remove(childHandler);
290 // check for flash briefing profile handler
291 if (childHandler instanceof FlashBriefingProfileHandler) {
292 flashBriefingProfileHandlers.remove(childHandler);
294 // check for flash briefing profile handler
295 if (childHandler instanceof SmartHomeDeviceHandler) {
296 smartHomeDeviceHandlers.remove(childHandler);
298 super.childHandlerDisposed(childHandler, childThing);
302 public void dispose() {
303 AccountServlet accountServlet = this.accountServlet;
304 if (accountServlet != null) {
305 accountServlet.dispose();
307 this.accountServlet = null;
312 private void cleanup() {
313 logger.debug("cleanup {}", getThing().getUID().getAsString());
314 ScheduledFuture<?> updateSmartHomeStateJob = this.updateSmartHomeStateJob;
315 if (updateSmartHomeStateJob != null) {
316 updateSmartHomeStateJob.cancel(true);
317 this.updateSmartHomeStateJob = null;
319 ScheduledFuture<?> refreshJob = this.checkDataJob;
320 if (refreshJob != null) {
321 refreshJob.cancel(true);
322 this.checkDataJob = null;
324 ScheduledFuture<?> refreshLogin = this.checkLoginJob;
325 if (refreshLogin != null) {
326 refreshLogin.cancel(true);
327 this.checkLoginJob = null;
329 ScheduledFuture<?> foceCheckDataJob = this.forceCheckDataJob;
330 if (foceCheckDataJob != null) {
331 foceCheckDataJob.cancel(true);
332 this.forceCheckDataJob = null;
334 ScheduledFuture<?> refreshAfterCommandJob = this.refreshAfterCommandJob;
335 if (refreshAfterCommandJob != null) {
336 refreshAfterCommandJob.cancel(true);
337 this.refreshAfterCommandJob = null;
339 ScheduledFuture<?> refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob;
340 if (refreshSmartHomeAfterCommandJob != null) {
341 refreshSmartHomeAfterCommandJob.cancel(true);
342 this.refreshSmartHomeAfterCommandJob = null;
344 Connection connection = this.connection;
345 if (connection != null) {
347 this.connection = null;
349 closeWebSocketConnection();
352 private void checkLogin() {
354 ThingUID uid = getThing().getUID();
355 logger.debug("check login {}", uid.getAsString());
357 synchronized (synchronizeConnection) {
358 Connection currentConnection = this.connection;
359 if (currentConnection == null) {
364 if (currentConnection.getIsLoggedIn()) {
365 if (currentConnection.checkRenewSession()) {
366 setConnection(currentConnection);
369 // read session data from property
370 String sessionStore = this.stateStorage.get("sessionStorage");
372 // try use the session data
373 if (currentConnection.tryRestoreLogin(sessionStore, null)) {
374 setConnection(currentConnection);
377 if (!currentConnection.getIsLoggedIn()) {
378 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
379 "Please login in through web site: http(s)://<YOUROPENHAB>:<YOURPORT>/amazonechocontrol/"
380 + URLEncoder.encode(uid.getId(), "UTF8"));
382 } catch (ConnectionException e) {
383 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
384 } catch (HttpException e) {
385 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
386 } catch (UnknownHostException e) {
387 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
388 "Unknown host name '" + e.getMessage() + "'. Maybe your internet connection is offline");
389 } catch (IOException e) {
390 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
391 } catch (URISyntaxException e) {
392 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
395 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
396 logger.error("check login fails with unexpected error", e);
400 // used to set a valid connection from the web proxy login
401 public void setConnection(@Nullable Connection connection) {
402 this.connection = connection;
403 if (connection != null) {
404 String serializedStorage = connection.serializeLoginData();
405 this.stateStorage.put("sessionStorage", serializedStorage);
407 this.stateStorage.put("sessionStorage", null);
408 updateStatus(ThingStatus.OFFLINE);
410 closeWebSocketConnection();
411 if (connection != null) {
413 updateSmartHomeDeviceList(false);
414 updateFlashBriefingHandlers();
415 updateStatus(ThingStatus.ONLINE);
421 void closeWebSocketConnection() {
422 WebSocketConnection webSocketConnection = this.webSocketConnection;
423 this.webSocketConnection = null;
424 if (webSocketConnection != null) {
425 webSocketConnection.close();
429 private boolean checkWebSocketConnection() {
430 WebSocketConnection webSocketConnection = this.webSocketConnection;
431 if (webSocketConnection == null || webSocketConnection.isClosed()) {
432 Connection connection = this.connection;
433 if (connection != null && connection.getIsLoggedIn()) {
435 this.webSocketConnection = new WebSocketConnection(connection.getAmazonSite(),
436 connection.getSessionCookies(), this);
437 } catch (IOException e) {
438 logger.warn("Web socket connection starting failed", e);
446 private void checkData() {
447 synchronized (synchronizeConnection) {
449 Connection connection = this.connection;
450 if (connection != null && connection.getIsLoggedIn()) {
452 if (checkDataCounter > 60 || forceCheckDataJob != null) {
453 checkDataCounter = 0;
454 forceCheckDataJob = null;
456 if (!checkWebSocketConnection() || checkDataCounter == 0) {
460 logger.debug("checkData {} finished", getThing().getUID().getAsString());
461 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
462 logger.debug("checkData fails", e);
463 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
464 logger.error("checkData fails with unexpected error", e);
469 private void refreshNotifications(@Nullable JsonCommandPayloadPushNotificationChange pushPayload) {
470 Connection currentConnection = this.connection;
471 if (currentConnection == null) {
474 if (!currentConnection.getIsLoggedIn()) {
477 JsonNotificationResponse[] notifications;
478 ZonedDateTime timeStamp = ZonedDateTime.now();
480 notifications = currentConnection.notifications();
481 } catch (IOException | URISyntaxException | InterruptedException e) {
482 logger.debug("refreshNotifications failed", e);
485 ZonedDateTime timeStampNow = ZonedDateTime.now();
487 echoHandlers.forEach(
488 echoHandler -> echoHandler.updateNotifications(timeStamp, timeStampNow, pushPayload, notifications));
491 private void refreshData() {
492 synchronized (synchronizeConnection) {
494 logger.debug("refreshing data {}", getThing().getUID().getAsString());
496 // check if logged in
497 Connection currentConnection = null;
498 currentConnection = connection;
499 if (currentConnection != null) {
500 if (!currentConnection.getIsLoggedIn()) {
504 if (currentConnection == null) {
508 // get all devices registered in the account
510 updateSmartHomeDeviceList(false);
511 updateFlashBriefingHandlers();
513 DeviceNotificationState[] deviceNotificationStates = null;
514 AscendingAlarmModel[] ascendingAlarmModels = null;
515 JsonBluetoothStates states = null;
516 List<JsonMusicProvider> musicProviders = null;
517 if (currentConnection.getIsLoggedIn()) {
518 // update notification states
519 deviceNotificationStates = currentConnection.getDeviceNotificationStates();
521 // update ascending alarm
522 ascendingAlarmModels = currentConnection.getAscendingAlarm();
524 // update bluetooth states
525 states = currentConnection.getBluetoothConnectionStates();
527 // update music providers
528 if (currentConnection.getIsLoggedIn()) {
530 musicProviders = currentConnection.getMusicProviders();
531 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
532 logger.debug("Update music provider failed", e);
536 // forward device information to echo handler
537 for (EchoHandler child : echoHandlers) {
538 Device device = findDeviceJson(child.findSerialNumber());
540 JsonNotificationSound[] notificationSounds = null;
541 JsonPlaylists playlists = null;
542 if (device != null && currentConnection.getIsLoggedIn()) {
543 // update notification sounds
545 notificationSounds = currentConnection.getNotificationSounds(device);
546 } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
547 logger.debug("Update notification sounds failed", e);
551 playlists = currentConnection.getPlaylists(device);
552 } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
553 logger.debug("Update playlist failed", e);
557 BluetoothState state = null;
558 if (states != null) {
559 state = states.findStateByDevice(device);
561 DeviceNotificationState deviceNotificationState = null;
562 AscendingAlarmModel ascendingAlarmModel = null;
563 if (device != null) {
564 final String serialNumber = device.serialNumber;
565 if (serialNumber != null) {
566 if (ascendingAlarmModels != null) {
567 ascendingAlarmModel = Arrays.stream(ascendingAlarmModels).filter(Objects::nonNull)
568 .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst()
571 if (deviceNotificationStates != null) {
572 deviceNotificationState = Arrays.stream(deviceNotificationStates)
573 .filter(Objects::nonNull)
574 .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst()
579 child.updateState(this, device, state, deviceNotificationState, ascendingAlarmModel, playlists,
580 notificationSounds, musicProviders);
583 // refresh notifications
584 refreshNotifications(null);
586 // update account state
587 updateStatus(ThingStatus.ONLINE);
589 logger.debug("refresh data {} finished", getThing().getUID().getAsString());
590 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
591 logger.debug("refresh data fails", e);
592 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
593 logger.error("refresh data fails with unexpected error", e);
598 public @Nullable Device findDeviceJson(@Nullable String serialNumber) {
599 if (serialNumber == null || serialNumber.isEmpty()) {
602 return this.jsonSerialNumberDeviceMapping.get(serialNumber);
605 public @Nullable Device findDeviceJsonBySerialOrName(@Nullable String serialOrName) {
606 if (serialOrName == null || serialOrName.isEmpty()) {
610 return this.jsonSerialNumberDeviceMapping.values().stream().filter(
611 d -> serialOrName.equalsIgnoreCase(d.serialNumber) || serialOrName.equalsIgnoreCase(d.accountName))
612 .findFirst().orElse(null);
615 public List<Device> updateDeviceList() {
616 Connection currentConnection = connection;
617 if (currentConnection == null) {
618 return new ArrayList<>();
621 List<Device> devices = null;
623 if (currentConnection.getIsLoggedIn()) {
624 devices = currentConnection.getDeviceList();
626 } catch (IOException | URISyntaxException | InterruptedException e) {
627 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
629 if (devices != null) {
630 // create new device map
631 jsonSerialNumberDeviceMapping = devices.stream().filter(device -> device.serialNumber != null)
632 .collect(Collectors.toMap(d -> Objects.requireNonNull(d.serialNumber), d -> d));
635 WakeWord[] wakeWords = currentConnection.getWakeWords();
637 for (EchoHandler echoHandler : echoHandlers) {
638 String serialNumber = echoHandler.findSerialNumber();
639 String deviceWakeWord = null;
640 for (WakeWord wakeWord : wakeWords) {
641 if (wakeWord != null) {
642 if (serialNumber.equals(wakeWord.deviceSerialNumber)) {
643 deviceWakeWord = wakeWord.wakeWord;
648 echoHandler.setDeviceAndUpdateThingState(this, findDeviceJson(serialNumber), deviceWakeWord);
651 if (devices != null) {
657 public void setEnabledFlashBriefingsJson(String flashBriefingJson) {
658 Connection currentConnection = connection;
659 JsonFeed[] feeds = gson.fromJson(flashBriefingJson, JsonFeed[].class);
660 if (currentConnection != null && feeds != null) {
662 currentConnection.setEnabledFlashBriefings(feeds);
663 } catch (IOException | URISyntaxException | InterruptedException e) {
664 logger.warn("Set flashbriefing profile failed", e);
667 updateFlashBriefingHandlers();
670 public String getNewCurrentFlashbriefingConfiguration() {
671 return updateFlashBriefingHandlers();
674 public String updateFlashBriefingHandlers() {
675 Connection currentConnection = connection;
676 if (currentConnection != null) {
677 return updateFlashBriefingHandlers(currentConnection);
682 private String updateFlashBriefingHandlers(Connection currentConnection) {
683 if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) {
684 updateFlashBriefingProfiles(currentConnection);
686 boolean flashBriefingProfileFound = false;
687 for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) {
688 flashBriefingProfileFound |= child.initialize(this, currentFlashBriefingJson);
690 if (flashBriefingProfileFound) {
693 return this.currentFlashBriefingJson;
696 public @Nullable Connection findConnection() {
697 return this.connection;
700 public String getEnabledFlashBriefingsJson() {
701 Connection currentConnection = this.connection;
702 if (currentConnection == null) {
705 updateFlashBriefingProfiles(currentConnection);
706 return this.currentFlashBriefingJson;
709 private void updateFlashBriefingProfiles(Connection currentConnection) {
711 JsonFeed[] feeds = currentConnection.getEnabledFlashBriefings();
712 // Make a copy and remove changeable parts
713 JsonFeed[] forSerializer = new JsonFeed[feeds.length];
714 for (int i = 0; i < feeds.length; i++) {
715 JsonFeed source = feeds[i];
716 JsonFeed copy = new JsonFeed();
717 copy.feedId = source.feedId;
718 copy.skillId = source.skillId;
719 // Do not copy imageUrl here, because it will change
720 forSerializer[i] = copy;
722 this.currentFlashBriefingJson = gson.toJson(forSerializer);
723 } catch (HttpException | JsonSyntaxException | IOException | URISyntaxException | ConnectionException
724 | InterruptedException e) {
725 logger.warn("get flash briefing profiles fails", e);
730 public void webSocketCommandReceived(JsonPushCommand pushCommand) {
732 handleWebsocketCommand(pushCommand);
733 } catch (Exception e) {
734 // should never happen, but if the exception is going out of this function, the binding stop working.
735 logger.warn("handling of websockets fails", e);
739 void handleWebsocketCommand(JsonPushCommand pushCommand) {
740 String command = pushCommand.command;
741 if (command != null) {
742 ScheduledFuture<?> refreshDataDelayed = this.refreshAfterCommandJob;
744 case "PUSH_ACTIVITY":
745 handlePushActivity(pushCommand.payload);
747 case "PUSH_DOPPLER_CONNECTION_CHANGE":
748 case "PUSH_BLUETOOTH_STATE_CHANGE":
749 if (refreshDataDelayed != null) {
750 refreshDataDelayed.cancel(false);
752 this.refreshAfterCommandJob = scheduler.schedule(this::refreshAfterCommand, 700,
753 TimeUnit.MILLISECONDS);
755 case "PUSH_NOTIFICATION_CHANGE":
756 JsonCommandPayloadPushNotificationChange pushPayload = gson.fromJson(pushCommand.payload,
757 JsonCommandPayloadPushNotificationChange.class);
758 refreshNotifications(pushPayload);
761 String payload = pushCommand.payload;
762 if (payload != null && payload.startsWith("{") && payload.endsWith("}")) {
763 JsonCommandPayloadPushDevice devicePayload = Objects
764 .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushDevice.class));
765 DopplerId dopplerId = devicePayload.dopplerId;
766 if (dopplerId != null) {
767 handlePushDeviceCommand(dopplerId, command, payload);
775 private void handlePushDeviceCommand(DopplerId dopplerId, String command, String payload) {
776 EchoHandler echoHandler = findEchoHandlerBySerialNumber(dopplerId.deviceSerialNumber);
777 if (echoHandler != null) {
778 echoHandler.handlePushCommand(command, payload);
782 private void handlePushActivity(@Nullable String payload) {
783 if (payload == null) {
786 JsonCommandPayloadPushActivity pushActivity = Objects
787 .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushActivity.class));
789 Key key = pushActivity.key;
794 Connection connection = this.connection;
795 if (connection == null || !connection.getIsLoggedIn()) {
799 String search = key.registeredUserId + "#" + key.entryId;
800 Arrays.stream(connection.getActivities(10, pushActivity.timestamp))
801 .filter(activity -> activity != null && search.equals(activity.id)).findFirst()
802 .ifPresent(currentActivity -> {
803 SourceDeviceId[] sourceDeviceIds = currentActivity.sourceDeviceIds;
804 if (sourceDeviceIds != null) {
805 Arrays.stream(sourceDeviceIds).filter(Objects::nonNull)
806 .map(sourceDeviceId -> findEchoHandlerBySerialNumber(sourceDeviceId.serialNumber))
807 .filter(Objects::nonNull).forEach(echoHandler -> Objects.requireNonNull(echoHandler)
808 .handlePushActivity(currentActivity));
813 void refreshAfterCommand() {
817 private @Nullable SmartHomeBaseDevice findSmartDeviceHomeJson(SmartHomeDeviceHandler handler) {
818 String id = handler.getId();
820 return jsonIdSmartHomeDeviceMapping.get(id);
825 public int getSmartHomeDevicesDiscoveryMode() {
826 return handlerConfig.discoverSmartHome;
829 public List<SmartHomeBaseDevice> updateSmartHomeDeviceList(boolean forceUpdate) {
830 Connection currentConnection = connection;
831 if (currentConnection == null) {
832 return Collections.emptyList();
835 if (!forceUpdate && smartHomeDeviceHandlers.isEmpty() && getSmartHomeDevicesDiscoveryMode() == 0) {
836 return Collections.emptyList();
839 List<SmartHomeBaseDevice> smartHomeDevices = null;
841 if (currentConnection.getIsLoggedIn()) {
842 smartHomeDevices = currentConnection.getSmarthomeDeviceList();
844 } catch (IOException | URISyntaxException | InterruptedException e) {
845 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
847 if (smartHomeDevices != null) {
849 Map<String, SmartHomeBaseDevice> newJsonIdSmartHomeDeviceMapping = new HashMap<>();
850 for (Object smartHomeDevice : smartHomeDevices) {
851 if (smartHomeDevice instanceof SmartHomeBaseDevice) {
852 SmartHomeBaseDevice smartHomeBaseDevice = (SmartHomeBaseDevice) smartHomeDevice;
853 String id = smartHomeBaseDevice.findId();
855 newJsonIdSmartHomeDeviceMapping.put(id, smartHomeBaseDevice);
859 jsonIdSmartHomeDeviceMapping = newJsonIdSmartHomeDeviceMapping;
862 smartHomeDeviceHandlers
863 .forEach(child -> child.setDeviceAndUpdateThingState(this, findSmartDeviceHomeJson(child)));
865 if (smartHomeDevices != null) {
866 return smartHomeDevices;
869 return Collections.emptyList();
872 public void forceDelayedSmartHomeStateUpdate(@Nullable String deviceId) {
873 if (deviceId == null) {
876 synchronized (synchronizeSmartHomeJobScheduler) {
877 requestedDeviceUpdates.add(deviceId);
878 ScheduledFuture<?> refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob;
879 if (refreshSmartHomeAfterCommandJob != null) {
880 refreshSmartHomeAfterCommandJob.cancel(false);
882 this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 500,
883 TimeUnit.MILLISECONDS);
887 private void updateSmartHomeStateJob() {
888 Set<String> deviceUpdates = new HashSet<>();
890 synchronized (synchronizeSmartHomeJobScheduler) {
891 Connection connection = this.connection;
892 if (connection == null || !connection.getIsLoggedIn()) {
893 this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 1000,
894 TimeUnit.MILLISECONDS);
897 requestedDeviceUpdates.drainTo(deviceUpdates);
898 this.refreshSmartHomeAfterCommandJob = null;
901 deviceUpdates.forEach(this::updateSmartHomeState);
904 private synchronized void updateSmartHomeState(@Nullable String deviceFilterId) {
906 logger.debug("updateSmartHomeState started with deviceFilterId={}", deviceFilterId);
907 Connection connection = this.connection;
908 if (connection == null || !connection.getIsLoggedIn()) {
911 List<SmartHomeBaseDevice> allDevices = getLastKnownSmartHomeDevices();
912 Set<String> applianceIds = new HashSet<>();
913 if (deviceFilterId != null) {
914 applianceIds.add(deviceFilterId);
916 SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator = this.smartHomeDeviceStateGroupUpdateCalculator;
917 if (smartHomeDeviceStateGroupUpdateCalculator == null) {
920 if (smartHomeDeviceHandlers.isEmpty()) {
923 List<SmartHomeDevice> devicesToUpdate = new ArrayList<>();
924 for (SmartHomeDeviceHandler device : smartHomeDeviceHandlers) {
925 String id = device.getId();
926 SmartHomeBaseDevice baseDevice = jsonIdSmartHomeDeviceMapping.get(id);
927 SmartHomeDeviceHandler.getSupportedSmartHomeDevices(baseDevice, allDevices)
928 .forEach(devicesToUpdate::add);
930 smartHomeDeviceStateGroupUpdateCalculator.removeDevicesWithNoUpdate(devicesToUpdate);
931 devicesToUpdate.stream().map(shd -> shd.applianceId).forEach(applianceId -> {
932 if (applianceId != null) {
933 applianceIds.add(applianceId);
936 if (applianceIds.isEmpty()) {
941 Map<String, JsonArray> applianceIdToCapabilityStates = connection
942 .getSmartHomeDeviceStatesJson(applianceIds);
944 for (SmartHomeDeviceHandler smartHomeDeviceHandler : smartHomeDeviceHandlers) {
945 String id = smartHomeDeviceHandler.getId();
946 if (requestedDeviceUpdates.contains(id)) {
947 logger.debug("Device update {} suspended", id);
950 if (deviceFilterId == null || id.equals(deviceFilterId)) {
951 smartHomeDeviceHandler.updateChannelStates(allDevices, applianceIdToCapabilityStates);
953 logger.trace("Id {} not matching filter {}", id, deviceFilterId);
957 logger.debug("updateSmartHomeState finished");
958 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
959 logger.debug("updateSmartHomeState fails", e);
960 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
961 logger.warn("updateSmartHomeState fails with unexpected error", e);