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 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;
28 import java.util.Optional;
30 import java.util.concurrent.CopyOnWriteArraySet;
31 import java.util.concurrent.LinkedBlockingQueue;
32 import java.util.concurrent.ScheduledFuture;
33 import java.util.concurrent.TimeUnit;
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 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<>();
107 private Map<String, SmartHomeDevice> jsonSerialNumberSmartHomeDeviceMapping = new HashMap<>();
109 private @Nullable ScheduledFuture<?> checkDataJob;
110 private @Nullable ScheduledFuture<?> checkLoginJob;
111 private @Nullable ScheduledFuture<?> updateSmartHomeStateJob;
112 private @Nullable ScheduledFuture<?> refreshAfterCommandJob;
113 private @Nullable ScheduledFuture<?> refreshSmartHomeAfterCommandJob;
114 private final Object synchronizeSmartHomeJobScheduler = new Object();
115 private @Nullable ScheduledFuture<?> forceCheckDataJob;
116 private String currentFlashBriefingJson = "";
117 private final HttpService httpService;
118 private @Nullable AccountServlet accountServlet;
119 private final Gson gson;
120 private int checkDataCounter;
121 private final LinkedBlockingQueue<String> requestedDeviceUpdates = new LinkedBlockingQueue<>();
122 private @Nullable SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator;
123 private List<ChannelHandler> channelHandlers = new ArrayList<>();
125 private AccountHandlerConfig handlerConfig = new AccountHandlerConfig();
127 public AccountHandler(Bridge bridge, HttpService httpService, Storage<String> stateStorage, Gson gson) {
130 this.httpService = httpService;
131 this.stateStorage = stateStorage;
132 channelHandlers.add(new ChannelHandlerSendMessage(this, this.gson));
136 public void initialize() {
137 handlerConfig = getConfig().as(AccountHandlerConfig.class);
139 synchronized (synchronizeConnection) {
140 Connection connection = this.connection;
141 if (connection == null) {
142 this.connection = new Connection(null, gson);
146 if (accountServlet == null) {
148 accountServlet = new AccountServlet(httpService, this.getThing().getUID().getId(), this, gson);
149 } catch (IllegalStateException e) {
150 logger.warn("Failed to create account servlet", e);
154 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login");
156 checkLoginJob = scheduler.scheduleWithFixedDelay(this::checkLogin, 0, 60, TimeUnit.SECONDS);
157 checkDataJob = scheduler.scheduleWithFixedDelay(this::checkData, 4, 60, TimeUnit.SECONDS);
159 int pollingIntervalAlexa = handlerConfig.pollingIntervalSmartHomeAlexa;
160 if (pollingIntervalAlexa < 10) {
161 pollingIntervalAlexa = 10;
163 int pollingIntervalSkills = handlerConfig.pollingIntervalSmartSkills;
164 if (pollingIntervalSkills < 60) {
165 pollingIntervalSkills = 60;
167 smartHomeDeviceStateGroupUpdateCalculator = new SmartHomeDeviceStateGroupUpdateCalculator(pollingIntervalAlexa,
168 pollingIntervalSkills);
169 updateSmartHomeStateJob = scheduler.scheduleWithFixedDelay(() -> updateSmartHomeState(null), 20, 10,
174 public void updateChannelState(String channelId, State state) {
175 updateState(channelId, state);
179 public void handleCommand(ChannelUID channelUID, Command command) {
181 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
182 Connection connection = this.connection;
183 if (connection == null) {
187 String channelId = channelUID.getId();
188 for (ChannelHandler channelHandler : channelHandlers) {
189 if (channelHandler.tryHandleCommand(new Device(), connection, channelId, command)) {
193 if (command instanceof RefreshType) {
196 } catch (IOException | URISyntaxException e) {
197 logger.info("handleCommand fails", e);
202 public void startAnnouncment(Device device, String speak, String bodyText, @Nullable String title,
203 @Nullable Integer volume) throws IOException, URISyntaxException {
204 EchoHandler echoHandler = findEchoHandlerBySerialNumber(device.serialNumber);
205 if (echoHandler != null) {
206 echoHandler.startAnnouncment(device, speak, bodyText, title, volume);
210 public List<FlashBriefingProfileHandler> getFlashBriefingProfileHandlers() {
211 return new ArrayList<>(flashBriefingProfileHandlers);
214 public List<Device> getLastKnownDevices() {
215 return new ArrayList<>(jsonSerialNumberDeviceMapping.values());
218 public List<SmartHomeBaseDevice> getLastKnownSmartHomeDevices() {
219 return new ArrayList<>(jsonIdSmartHomeDeviceMapping.values());
222 public void addEchoHandler(EchoHandler echoHandler) {
223 if (echoHandlers.add(echoHandler)) {
229 public void addSmartHomeDeviceHandler(SmartHomeDeviceHandler smartHomeDeviceHandler) {
230 if (smartHomeDeviceHandlers.add(smartHomeDeviceHandler)) {
235 public void forceCheckData() {
236 if (forceCheckDataJob == null) {
237 forceCheckDataJob = scheduler.schedule(this::checkData, 1000, TimeUnit.MILLISECONDS);
241 public @Nullable Thing findThingBySerialNumber(@Nullable String deviceSerialNumber) {
242 EchoHandler echoHandler = findEchoHandlerBySerialNumber(deviceSerialNumber);
243 if (echoHandler != null) {
244 return echoHandler.getThing();
249 public @Nullable EchoHandler findEchoHandlerBySerialNumber(@Nullable String deviceSerialNumber) {
250 for (EchoHandler echoHandler : echoHandlers) {
251 if (deviceSerialNumber != null && deviceSerialNumber.equals(echoHandler.findSerialNumber())) {
258 public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBriefingProfileHandler) {
259 flashBriefingProfileHandlers.add(flashBriefingProfileHandler);
260 Connection connection = this.connection;
261 if (connection != null && connection.getIsLoggedIn()) {
262 if (currentFlashBriefingJson.isEmpty()) {
263 updateFlashBriefingProfiles(connection);
265 flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson);
269 private void scheduleUpdate() {
270 checkDataCounter = 999;
274 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
275 super.childHandlerInitialized(childHandler, childThing);
280 public void handleRemoval() {
282 super.handleRemoval();
286 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
287 // check for echo handler
288 if (childHandler instanceof EchoHandler) {
289 echoHandlers.remove(childHandler);
291 // check for flash briefing profile handler
292 if (childHandler instanceof FlashBriefingProfileHandler) {
293 flashBriefingProfileHandlers.remove(childHandler);
295 // check for flash briefing profile handler
296 if (childHandler instanceof SmartHomeDeviceHandler) {
297 smartHomeDeviceHandlers.remove(childHandler);
299 super.childHandlerDisposed(childHandler, childThing);
303 public void dispose() {
304 AccountServlet accountServlet = this.accountServlet;
305 if (accountServlet != null) {
306 accountServlet.dispose();
308 this.accountServlet = null;
313 private void cleanup() {
314 logger.debug("cleanup {}", getThing().getUID().getAsString());
315 ScheduledFuture<?> updateSmartHomeStateJob = this.updateSmartHomeStateJob;
316 if (updateSmartHomeStateJob != null) {
317 updateSmartHomeStateJob.cancel(true);
318 this.updateSmartHomeStateJob = null;
320 ScheduledFuture<?> refreshJob = this.checkDataJob;
321 if (refreshJob != null) {
322 refreshJob.cancel(true);
323 this.checkDataJob = null;
325 ScheduledFuture<?> refreshLogin = this.checkLoginJob;
326 if (refreshLogin != null) {
327 refreshLogin.cancel(true);
328 this.checkLoginJob = null;
330 ScheduledFuture<?> foceCheckDataJob = this.forceCheckDataJob;
331 if (foceCheckDataJob != null) {
332 foceCheckDataJob.cancel(true);
333 this.forceCheckDataJob = null;
335 ScheduledFuture<?> refreshAfterCommandJob = this.refreshAfterCommandJob;
336 if (refreshAfterCommandJob != null) {
337 refreshAfterCommandJob.cancel(true);
338 this.refreshAfterCommandJob = null;
340 ScheduledFuture<?> refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob;
341 if (refreshSmartHomeAfterCommandJob != null) {
342 refreshSmartHomeAfterCommandJob.cancel(true);
343 this.refreshSmartHomeAfterCommandJob = null;
345 Connection connection = this.connection;
346 if (connection != null) {
348 this.connection = null;
350 closeWebSocketConnection();
353 private void checkLogin() {
355 ThingUID uid = getThing().getUID();
356 logger.debug("check login {}", uid.getAsString());
358 synchronized (synchronizeConnection) {
359 Connection currentConnection = this.connection;
360 if (currentConnection == null) {
365 if (currentConnection.getIsLoggedIn()) {
366 if (currentConnection.checkRenewSession()) {
367 setConnection(currentConnection);
370 // read session data from property
371 String sessionStore = this.stateStorage.get("sessionStorage");
373 // try use the session data
374 if (currentConnection.tryRestoreLogin(sessionStore, null)) {
375 setConnection(currentConnection);
378 if (!currentConnection.getIsLoggedIn()) {
379 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
380 "Please login in through web site: http(s)://<YOUROPENHAB>:<YOURPORT>/amazonechocontrol/"
381 + URLEncoder.encode(uid.getId(), "UTF8"));
383 } catch (ConnectionException e) {
384 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
385 } catch (HttpException e) {
386 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
387 } catch (UnknownHostException e) {
388 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
389 "Unknown host name '" + e.getMessage() + "'. Maybe your internet connection is offline");
390 } catch (IOException e) {
391 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
392 } catch (URISyntaxException e) {
393 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
396 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
397 logger.error("check login fails with unexpected error", e);
401 // used to set a valid connection from the web proxy login
402 public void setConnection(@Nullable Connection connection) {
403 this.connection = connection;
404 if (connection != null) {
405 String serializedStorage = connection.serializeLoginData();
406 this.stateStorage.put("sessionStorage", serializedStorage);
408 this.stateStorage.put("sessionStorage", null);
409 updateStatus(ThingStatus.OFFLINE);
411 closeWebSocketConnection();
412 if (connection != null) {
414 updateSmartHomeDeviceList(false);
415 updateFlashBriefingHandlers();
416 updateStatus(ThingStatus.ONLINE);
422 void closeWebSocketConnection() {
423 WebSocketConnection webSocketConnection = this.webSocketConnection;
424 this.webSocketConnection = null;
425 if (webSocketConnection != null) {
426 webSocketConnection.close();
430 private boolean checkWebSocketConnection() {
431 WebSocketConnection webSocketConnection = this.webSocketConnection;
432 if (webSocketConnection == null || webSocketConnection.isClosed()) {
433 Connection connection = this.connection;
434 if (connection != null && connection.getIsLoggedIn()) {
436 this.webSocketConnection = new WebSocketConnection(connection.getAmazonSite(),
437 connection.getSessionCookies(), this);
438 } catch (IOException e) {
439 logger.warn("Web socket connection starting failed", e);
447 private void checkData() {
448 synchronized (synchronizeConnection) {
450 Connection connection = this.connection;
451 if (connection != null && connection.getIsLoggedIn()) {
453 if (checkDataCounter > 60 || forceCheckDataJob != null) {
454 checkDataCounter = 0;
455 forceCheckDataJob = null;
457 if (!checkWebSocketConnection() || checkDataCounter == 0) {
461 logger.debug("checkData {} finished", getThing().getUID().getAsString());
462 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
463 logger.debug("checkData fails", e);
464 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
465 logger.error("checkData fails with unexpected error", e);
470 private void refreshNotifications(@Nullable JsonCommandPayloadPushNotificationChange pushPayload) {
471 Connection currentConnection = this.connection;
472 if (currentConnection == null) {
475 if (!currentConnection.getIsLoggedIn()) {
478 JsonNotificationResponse[] notifications;
479 ZonedDateTime timeStamp = ZonedDateTime.now();
481 notifications = currentConnection.notifications();
482 } catch (IOException | URISyntaxException e) {
483 logger.debug("refreshNotifications failed", e);
486 ZonedDateTime timeStampNow = ZonedDateTime.now();
488 echoHandlers.forEach(
489 echoHandler -> echoHandler.updateNotifications(timeStamp, timeStampNow, pushPayload, notifications));
492 private void refreshData() {
493 synchronized (synchronizeConnection) {
495 logger.debug("refreshing data {}", getThing().getUID().getAsString());
497 // check if logged in
498 Connection currentConnection = null;
499 currentConnection = connection;
500 if (currentConnection != null) {
501 if (!currentConnection.getIsLoggedIn()) {
505 if (currentConnection == null) {
509 // get all devices registered in the account
511 updateSmartHomeDeviceList(false);
512 updateFlashBriefingHandlers();
514 DeviceNotificationState[] deviceNotificationStates = null;
515 AscendingAlarmModel[] ascendingAlarmModels = null;
516 JsonBluetoothStates states = null;
517 List<JsonMusicProvider> musicProviders = null;
518 if (currentConnection.getIsLoggedIn()) {
519 // update notification states
520 deviceNotificationStates = currentConnection.getDeviceNotificationStates();
522 // update ascending alarm
523 ascendingAlarmModels = currentConnection.getAscendingAlarm();
525 // update bluetooth states
526 states = currentConnection.getBluetoothConnectionStates();
528 // update music providers
529 if (currentConnection.getIsLoggedIn()) {
531 musicProviders = currentConnection.getMusicProviders();
532 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
533 logger.debug("Update music provider failed", e);
537 // forward device information to echo handler
538 for (EchoHandler child : echoHandlers) {
539 Device device = findDeviceJson(child.findSerialNumber());
541 JsonNotificationSound[] notificationSounds = null;
542 JsonPlaylists playlists = null;
543 if (device != null && currentConnection.getIsLoggedIn()) {
544 // update notification sounds
546 notificationSounds = currentConnection.getNotificationSounds(device);
547 } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
548 logger.debug("Update notification sounds failed", e);
552 playlists = currentConnection.getPlaylists(device);
553 } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
554 logger.debug("Update playlist failed", e);
558 BluetoothState state = null;
559 if (states != null) {
560 state = states.findStateByDevice(device);
562 DeviceNotificationState deviceNotificationState = null;
563 AscendingAlarmModel ascendingAlarmModel = null;
564 if (device != null) {
565 final String serialNumber = device.serialNumber;
566 if (serialNumber != null) {
567 if (ascendingAlarmModels != null) {
568 ascendingAlarmModel = Arrays.stream(ascendingAlarmModels).filter(Objects::nonNull)
569 .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst()
572 if (deviceNotificationStates != null) {
573 deviceNotificationState = Arrays.stream(deviceNotificationStates)
574 .filter(Objects::nonNull)
575 .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst()
580 child.updateState(this, device, state, deviceNotificationState, ascendingAlarmModel, playlists,
581 notificationSounds, musicProviders);
584 // refresh notifications
585 refreshNotifications(null);
587 // update account state
588 updateStatus(ThingStatus.ONLINE);
590 logger.debug("refresh data {} finished", getThing().getUID().getAsString());
591 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
592 logger.debug("refresh data fails", e);
593 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
594 logger.error("refresh data fails with unexpected error", e);
599 public @Nullable Device findDeviceJson(@Nullable String serialNumber) {
600 Device result = null;
601 if (serialNumber != null && !serialNumber.isEmpty()) {
602 Map<String, Device> jsonSerialNumberDeviceMapping = this.jsonSerialNumberDeviceMapping;
603 result = jsonSerialNumberDeviceMapping.get(serialNumber);
608 public @Nullable Device findDeviceJsonBySerialOrName(@Nullable String serialOrName) {
609 if (serialOrName == null || serialOrName.isEmpty()) {
613 Optional<Device> device = this.jsonSerialNumberDeviceMapping.values().stream().filter(
614 d -> serialOrName.equalsIgnoreCase(d.serialNumber) || serialOrName.equalsIgnoreCase(d.accountName))
617 if (device.isPresent()) {
624 public List<Device> updateDeviceList() {
625 Connection currentConnection = connection;
626 if (currentConnection == null) {
627 return new ArrayList<>();
630 List<Device> devices = null;
632 if (currentConnection.getIsLoggedIn()) {
633 devices = currentConnection.getDeviceList();
635 } catch (IOException | URISyntaxException e) {
636 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
638 if (devices != null) {
639 // create new device map
640 Map<String, Device> newJsonSerialDeviceMapping = new HashMap<>();
641 for (Device device : devices) {
642 String serialNumber = device.serialNumber;
643 if (serialNumber != null) {
644 newJsonSerialDeviceMapping.put(serialNumber, device);
648 jsonSerialNumberDeviceMapping = newJsonSerialDeviceMapping;
651 WakeWord[] wakeWords = currentConnection.getWakeWords();
653 for (EchoHandler echoHandler : echoHandlers) {
654 String serialNumber = echoHandler.findSerialNumber();
655 String deviceWakeWord = null;
656 for (WakeWord wakeWord : wakeWords) {
657 if (wakeWord != null) {
658 if (serialNumber != null && serialNumber.equals(wakeWord.deviceSerialNumber)) {
659 deviceWakeWord = wakeWord.wakeWord;
664 echoHandler.setDeviceAndUpdateThingState(this, findDeviceJson(serialNumber), deviceWakeWord);
667 if (devices != null) {
670 return Collections.emptyList();
673 public void setEnabledFlashBriefingsJson(String flashBriefingJson) {
674 Connection currentConnection = connection;
675 JsonFeed[] feeds = gson.fromJson(flashBriefingJson, JsonFeed[].class);
676 if (currentConnection != null && feeds != null) {
678 currentConnection.setEnabledFlashBriefings(feeds);
679 } catch (IOException | URISyntaxException e) {
680 logger.warn("Set flashbriefing profile failed", e);
683 updateFlashBriefingHandlers();
686 public String getNewCurrentFlashbriefingConfiguration() {
687 return updateFlashBriefingHandlers();
690 public String updateFlashBriefingHandlers() {
691 Connection currentConnection = connection;
692 if (currentConnection != null) {
693 return updateFlashBriefingHandlers(currentConnection);
698 private String updateFlashBriefingHandlers(Connection currentConnection) {
699 if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) {
700 updateFlashBriefingProfiles(currentConnection);
702 boolean flashBriefingProfileFound = false;
703 for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) {
704 flashBriefingProfileFound |= child.initialize(this, currentFlashBriefingJson);
706 if (flashBriefingProfileFound) {
709 return this.currentFlashBriefingJson;
712 public @Nullable Connection findConnection() {
713 return this.connection;
716 public String getEnabledFlashBriefingsJson() {
717 Connection currentConnection = this.connection;
718 if (currentConnection == null) {
721 updateFlashBriefingProfiles(currentConnection);
722 return this.currentFlashBriefingJson;
725 private void updateFlashBriefingProfiles(Connection currentConnection) {
727 JsonFeed[] feeds = currentConnection.getEnabledFlashBriefings();
728 // Make a copy and remove changeable parts
729 JsonFeed[] forSerializer = new JsonFeed[feeds.length];
730 for (int i = 0; i < feeds.length; i++) {
731 JsonFeed source = feeds[i];
732 JsonFeed copy = new JsonFeed();
733 copy.feedId = source.feedId;
734 copy.skillId = source.skillId;
735 // Do not copy imageUrl here, because it will change
736 forSerializer[i] = copy;
738 this.currentFlashBriefingJson = gson.toJson(forSerializer);
739 } catch (HttpException | JsonSyntaxException | IOException | URISyntaxException | ConnectionException e) {
740 logger.warn("get flash briefing profiles fails", e);
745 public void webSocketCommandReceived(JsonPushCommand pushCommand) {
747 handleWebsocketCommand(pushCommand);
748 } catch (Exception e) {
749 // should never happen, but if the exception is going out of this function, the binding stop working.
750 logger.warn("handling of websockets fails", e);
754 void handleWebsocketCommand(JsonPushCommand pushCommand) {
755 String command = pushCommand.command;
756 if (command != null) {
757 ScheduledFuture<?> refreshDataDelayed = this.refreshAfterCommandJob;
759 case "PUSH_ACTIVITY":
760 handlePushActivity(pushCommand.payload);
761 if (refreshDataDelayed != null) {
762 refreshDataDelayed.cancel(false);
764 this.refreshAfterCommandJob = scheduler.schedule(this::refreshAfterCommand, 700,
765 TimeUnit.MILLISECONDS);
767 case "PUSH_DOPPLER_CONNECTION_CHANGE":
768 case "PUSH_BLUETOOTH_STATE_CHANGE":
769 if (refreshDataDelayed != null) {
770 refreshDataDelayed.cancel(false);
772 this.refreshAfterCommandJob = scheduler.schedule(this::refreshAfterCommand, 700,
773 TimeUnit.MILLISECONDS);
775 case "PUSH_NOTIFICATION_CHANGE":
776 JsonCommandPayloadPushNotificationChange pushPayload = gson.fromJson(pushCommand.payload,
777 JsonCommandPayloadPushNotificationChange.class);
778 refreshNotifications(pushPayload);
781 String payload = pushCommand.payload;
782 if (payload != null && payload.startsWith("{") && payload.endsWith("}")) {
783 JsonCommandPayloadPushDevice devicePayload = gson.fromJson(payload,
784 JsonCommandPayloadPushDevice.class);
785 DopplerId dopplerId = devicePayload.dopplerId;
786 if (dopplerId != null) {
787 handlePushDeviceCommand(dopplerId, command, payload);
795 private void handlePushDeviceCommand(DopplerId dopplerId, String command, String payload) {
796 EchoHandler echoHandler = findEchoHandlerBySerialNumber(dopplerId.deviceSerialNumber);
797 if (echoHandler != null) {
798 echoHandler.handlePushCommand(command, payload);
802 private void handlePushActivity(@Nullable String payload) {
803 JsonCommandPayloadPushActivity pushActivity = gson.fromJson(payload, JsonCommandPayloadPushActivity.class);
805 Key key = pushActivity.key;
810 Connection connection = this.connection;
811 if (connection == null || !connection.getIsLoggedIn()) {
815 String search = key.registeredUserId + "#" + key.entryId;
816 Arrays.stream(connection.getActivities(10, pushActivity.timestamp))
817 .filter(activity -> activity != null && search.equals(activity.id)).findFirst()
818 .ifPresent(currentActivity -> {
819 SourceDeviceId[] sourceDeviceIds = currentActivity.sourceDeviceIds;
820 if (sourceDeviceIds != null) {
821 Arrays.stream(sourceDeviceIds).filter(Objects::nonNull)
822 .map(sourceDeviceId -> findEchoHandlerBySerialNumber(sourceDeviceId.serialNumber))
823 .filter(Objects::nonNull)
824 .forEach(echoHandler -> echoHandler.handlePushActivity(currentActivity));
829 void refreshAfterCommand() {
833 private @Nullable SmartHomeBaseDevice findSmartDeviceHomeJson(SmartHomeDeviceHandler handler) {
834 String id = handler.getId();
836 return jsonIdSmartHomeDeviceMapping.get(id);
841 public int getSmartHomeDevicesDiscoveryMode() {
842 return handlerConfig.discoverSmartHome;
845 public List<SmartHomeBaseDevice> updateSmartHomeDeviceList(boolean forceUpdate) {
846 Connection currentConnection = connection;
847 if (currentConnection == null) {
848 return Collections.emptyList();
851 if (!forceUpdate && smartHomeDeviceHandlers.isEmpty() && getSmartHomeDevicesDiscoveryMode() == 0) {
852 return Collections.emptyList();
855 List<SmartHomeBaseDevice> smartHomeDevices = null;
857 if (currentConnection.getIsLoggedIn()) {
858 smartHomeDevices = currentConnection.getSmarthomeDeviceList();
860 } catch (IOException | URISyntaxException e) {
861 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
863 if (smartHomeDevices != null) {
865 Map<String, SmartHomeBaseDevice> newJsonIdSmartHomeDeviceMapping = new HashMap<>();
866 for (Object smartHomeDevice : smartHomeDevices) {
867 if (smartHomeDevice instanceof SmartHomeBaseDevice) {
868 SmartHomeBaseDevice smartHomeBaseDevice = (SmartHomeBaseDevice) smartHomeDevice;
869 String id = smartHomeBaseDevice.findId();
871 newJsonIdSmartHomeDeviceMapping.put(id, smartHomeBaseDevice);
875 jsonIdSmartHomeDeviceMapping = newJsonIdSmartHomeDeviceMapping;
878 smartHomeDeviceHandlers
879 .forEach(child -> child.setDeviceAndUpdateThingState(this, findSmartDeviceHomeJson(child)));
881 if (smartHomeDevices != null) {
882 Map<String, SmartHomeDevice> newJsonSerialNumberSmartHomeDeviceMapping = new HashMap<>();
883 for (Object smartDevice : smartHomeDevices) {
884 if (smartDevice instanceof SmartHomeDevice) {
885 SmartHomeDevice shd = (SmartHomeDevice) smartDevice;
886 String entityId = shd.entityId;
887 if (entityId != null) {
888 newJsonSerialNumberSmartHomeDeviceMapping.put(entityId, shd);
892 jsonSerialNumberSmartHomeDeviceMapping = newJsonSerialNumberSmartHomeDeviceMapping;
894 if (smartHomeDevices != null) {
895 return smartHomeDevices;
898 return Collections.emptyList();
901 public void forceDelayedSmartHomeStateUpdate(@Nullable String deviceId) {
902 if (deviceId == null) {
905 synchronized (synchronizeSmartHomeJobScheduler) {
906 requestedDeviceUpdates.add(deviceId);
907 ScheduledFuture<?> refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob;
908 if (refreshSmartHomeAfterCommandJob != null) {
909 refreshSmartHomeAfterCommandJob.cancel(false);
911 this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 500,
912 TimeUnit.MILLISECONDS);
916 private void updateSmartHomeStateJob() {
917 Set<String> deviceUpdates = new HashSet<>();
919 synchronized (synchronizeSmartHomeJobScheduler) {
920 Connection connection = this.connection;
921 if (connection == null || !connection.getIsLoggedIn()) {
922 this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 1000,
923 TimeUnit.MILLISECONDS);
926 requestedDeviceUpdates.drainTo(deviceUpdates);
927 this.refreshSmartHomeAfterCommandJob = null;
930 deviceUpdates.forEach(this::updateSmartHomeState);
933 private synchronized void updateSmartHomeState(@Nullable String deviceFilterId) {
935 logger.debug("updateSmartHomeState started");
936 Connection connection = this.connection;
937 if (connection == null || !connection.getIsLoggedIn()) {
940 List<SmartHomeBaseDevice> allDevices = getLastKnownSmartHomeDevices();
941 Set<String> applianceIds = new HashSet<>();
942 if (deviceFilterId != null) {
943 applianceIds.add(deviceFilterId);
945 SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator = this.smartHomeDeviceStateGroupUpdateCalculator;
946 if (smartHomeDeviceStateGroupUpdateCalculator == null) {
949 if (smartHomeDeviceHandlers.isEmpty()) {
952 List<SmartHomeDevice> devicesToUpdate = new ArrayList<>();
953 for (SmartHomeDeviceHandler device : smartHomeDeviceHandlers) {
954 String id = device.getId();
955 SmartHomeBaseDevice baseDevice = jsonIdSmartHomeDeviceMapping.get(id);
956 SmartHomeDeviceHandler.getSupportedSmartHomeDevices(baseDevice, allDevices)
957 .forEach(devicesToUpdate::add);
959 smartHomeDeviceStateGroupUpdateCalculator.removeDevicesWithNoUpdate(devicesToUpdate);
960 devicesToUpdate.stream().map(shd -> shd.applianceId).forEach(applianceId -> {
961 if (applianceId != null) {
962 applianceIds.add(applianceId);
965 if (applianceIds.isEmpty()) {
970 Map<String, JsonArray> applianceIdToCapabilityStates = connection
971 .getSmartHomeDeviceStatesJson(applianceIds);
973 for (SmartHomeDeviceHandler smartHomeDeviceHandler : smartHomeDeviceHandlers) {
974 String id = smartHomeDeviceHandler.getId();
975 if (requestedDeviceUpdates.contains(id)) {
976 logger.debug("Device update {} suspended", id);
979 if (id.equals(deviceFilterId)) {
980 smartHomeDeviceHandler.updateChannelStates(allDevices, applianceIdToCapabilityStates);
984 logger.debug("updateSmartHomeState finished");
985 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
986 logger.debug("updateSmartHomeState fails", e);
987 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
988 logger.warn("updateSmartHomeState fails with unexpected error", e);