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;
21 import java.util.concurrent.CopyOnWriteArraySet;
22 import java.util.concurrent.LinkedBlockingQueue;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.stream.Collectors;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.amazonechocontrol.internal.AccountHandlerConfig;
30 import org.openhab.binding.amazonechocontrol.internal.AccountServlet;
31 import org.openhab.binding.amazonechocontrol.internal.Connection;
32 import org.openhab.binding.amazonechocontrol.internal.ConnectionException;
33 import org.openhab.binding.amazonechocontrol.internal.HttpException;
34 import org.openhab.binding.amazonechocontrol.internal.IWebSocketCommandHandler;
35 import org.openhab.binding.amazonechocontrol.internal.WebSocketConnection;
36 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandler;
37 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandlerSendMessage;
38 import org.openhab.binding.amazonechocontrol.internal.channelhandler.IAmazonThingHandler;
39 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm.AscendingAlarmModel;
40 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates;
41 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState;
42 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushActivity;
43 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushActivity.Key;
44 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushDevice;
45 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushDevice.DopplerId;
46 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushNotificationChange;
47 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState.DeviceNotificationState;
48 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device;
49 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed;
50 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider;
51 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse;
52 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound;
53 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists;
54 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPushCommand;
55 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice;
56 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonWakeWords.WakeWord;
57 import org.openhab.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice;
58 import org.openhab.binding.amazonechocontrol.internal.smarthome.SmartHomeDeviceStateGroupUpdateCalculator;
59 import org.openhab.core.storage.Storage;
60 import org.openhab.core.thing.Bridge;
61 import org.openhab.core.thing.ChannelUID;
62 import org.openhab.core.thing.Thing;
63 import org.openhab.core.thing.ThingStatus;
64 import org.openhab.core.thing.ThingStatusDetail;
65 import org.openhab.core.thing.ThingUID;
66 import org.openhab.core.thing.binding.BaseBridgeHandler;
67 import org.openhab.core.thing.binding.ThingHandler;
68 import org.openhab.core.types.Command;
69 import org.openhab.core.types.RefreshType;
70 import org.openhab.core.types.State;
71 import org.osgi.service.http.HttpService;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
75 import com.google.gson.Gson;
76 import com.google.gson.JsonArray;
77 import com.google.gson.JsonSyntaxException;
80 * Handles the connection to the amazon server.
82 * @author Michael Geramb - Initial Contribution
85 public class AccountHandler extends BaseBridgeHandler implements IWebSocketCommandHandler, IAmazonThingHandler {
86 private final Logger logger = LoggerFactory.getLogger(AccountHandler.class);
87 private final Storage<String> stateStorage;
88 private @Nullable Connection connection;
89 private @Nullable WebSocketConnection webSocketConnection;
91 private final Set<EchoHandler> echoHandlers = new CopyOnWriteArraySet<>();
92 private final Set<SmartHomeDeviceHandler> smartHomeDeviceHandlers = new CopyOnWriteArraySet<>();
93 private final Set<FlashBriefingProfileHandler> flashBriefingProfileHandlers = new CopyOnWriteArraySet<>();
95 private final Object synchronizeConnection = new Object();
96 private Map<String, Device> jsonSerialNumberDeviceMapping = new HashMap<>();
97 private Map<String, SmartHomeBaseDevice> jsonIdSmartHomeDeviceMapping = new HashMap<>();
99 private @Nullable ScheduledFuture<?> checkDataJob;
100 private @Nullable ScheduledFuture<?> checkLoginJob;
101 private @Nullable ScheduledFuture<?> updateSmartHomeStateJob;
102 private @Nullable ScheduledFuture<?> refreshAfterCommandJob;
103 private @Nullable ScheduledFuture<?> refreshSmartHomeAfterCommandJob;
104 private final Object synchronizeSmartHomeJobScheduler = new Object();
105 private @Nullable ScheduledFuture<?> forceCheckDataJob;
106 private String currentFlashBriefingJson = "";
107 private final HttpService httpService;
108 private @Nullable AccountServlet accountServlet;
109 private final Gson gson;
110 private int checkDataCounter;
111 private final LinkedBlockingQueue<String> requestedDeviceUpdates = new LinkedBlockingQueue<>();
112 private @Nullable SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator;
113 private List<ChannelHandler> channelHandlers = new ArrayList<>();
115 private AccountHandlerConfig handlerConfig = new AccountHandlerConfig();
117 public AccountHandler(Bridge bridge, HttpService httpService, Storage<String> stateStorage, Gson gson) {
120 this.httpService = httpService;
121 this.stateStorage = stateStorage;
122 channelHandlers.add(new ChannelHandlerSendMessage(this, this.gson));
126 public void initialize() {
127 handlerConfig = getConfig().as(AccountHandlerConfig.class);
129 synchronized (synchronizeConnection) {
130 Connection connection = this.connection;
131 if (connection == null) {
132 this.connection = new Connection(null, gson);
136 if (accountServlet == null) {
138 accountServlet = new AccountServlet(httpService, this.getThing().getUID().getId(), this, gson);
139 } catch (IllegalStateException e) {
140 logger.warn("Failed to create account servlet", e);
144 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login");
146 checkLoginJob = scheduler.scheduleWithFixedDelay(this::checkLogin, 0, 60, TimeUnit.SECONDS);
147 checkDataJob = scheduler.scheduleWithFixedDelay(this::checkData, 4, 60, TimeUnit.SECONDS);
149 int pollingIntervalAlexa = handlerConfig.pollingIntervalSmartHomeAlexa;
150 if (pollingIntervalAlexa < 10) {
151 pollingIntervalAlexa = 10;
153 int pollingIntervalSkills = handlerConfig.pollingIntervalSmartSkills;
154 if (pollingIntervalSkills < 60) {
155 pollingIntervalSkills = 60;
157 smartHomeDeviceStateGroupUpdateCalculator = new SmartHomeDeviceStateGroupUpdateCalculator(pollingIntervalAlexa,
158 pollingIntervalSkills);
159 updateSmartHomeStateJob = scheduler.scheduleWithFixedDelay(() -> updateSmartHomeState(null), 20, 10,
164 public void updateChannelState(String channelId, State state) {
165 updateState(channelId, state);
169 public void handleCommand(ChannelUID channelUID, Command command) {
171 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
172 Connection connection = this.connection;
173 if (connection == null) {
177 String channelId = channelUID.getId();
178 for (ChannelHandler channelHandler : channelHandlers) {
179 if (channelHandler.tryHandleCommand(new Device(), connection, channelId, command)) {
183 if (command instanceof RefreshType) {
186 } catch (IOException | URISyntaxException | InterruptedException e) {
187 logger.info("handleCommand fails", e);
192 public void startAnnouncement(Device device, String speak, String bodyText, @Nullable String title,
193 @Nullable Integer volume) throws IOException, URISyntaxException {
194 EchoHandler echoHandler = findEchoHandlerBySerialNumber(device.serialNumber);
195 if (echoHandler != null) {
196 echoHandler.startAnnouncement(device, speak, bodyText, title, volume);
200 public List<FlashBriefingProfileHandler> getFlashBriefingProfileHandlers() {
201 return new ArrayList<>(flashBriefingProfileHandlers);
204 public List<Device> getLastKnownDevices() {
205 return new ArrayList<>(jsonSerialNumberDeviceMapping.values());
208 public List<SmartHomeBaseDevice> getLastKnownSmartHomeDevices() {
209 return new ArrayList<>(jsonIdSmartHomeDeviceMapping.values());
212 public void addEchoHandler(EchoHandler echoHandler) {
213 if (echoHandlers.add(echoHandler)) {
218 public void addSmartHomeDeviceHandler(SmartHomeDeviceHandler smartHomeDeviceHandler) {
219 if (smartHomeDeviceHandlers.add(smartHomeDeviceHandler)) {
224 public void forceCheckData() {
225 if (forceCheckDataJob == null) {
226 forceCheckDataJob = scheduler.schedule(this::checkData, 1000, TimeUnit.MILLISECONDS);
230 public @Nullable Thing findThingBySerialNumber(@Nullable String deviceSerialNumber) {
231 EchoHandler echoHandler = findEchoHandlerBySerialNumber(deviceSerialNumber);
232 if (echoHandler != null) {
233 return echoHandler.getThing();
238 public @Nullable EchoHandler findEchoHandlerBySerialNumber(@Nullable String deviceSerialNumber) {
239 for (EchoHandler echoHandler : echoHandlers) {
240 if (deviceSerialNumber != null && deviceSerialNumber.equals(echoHandler.findSerialNumber())) {
247 public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBriefingProfileHandler) {
248 flashBriefingProfileHandlers.add(flashBriefingProfileHandler);
249 Connection connection = this.connection;
250 if (connection != null && connection.getIsLoggedIn()) {
251 if (currentFlashBriefingJson.isEmpty()) {
252 updateFlashBriefingProfiles(connection);
254 flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson);
258 private void scheduleUpdate() {
259 checkDataCounter = 999;
263 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
264 super.childHandlerInitialized(childHandler, childThing);
269 public void handleRemoval() {
271 super.handleRemoval();
275 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
276 // check for echo handler
277 if (childHandler instanceof EchoHandler) {
278 echoHandlers.remove(childHandler);
280 // check for flash briefing profile handler
281 if (childHandler instanceof FlashBriefingProfileHandler) {
282 flashBriefingProfileHandlers.remove(childHandler);
284 // check for flash briefing profile handler
285 if (childHandler instanceof SmartHomeDeviceHandler) {
286 smartHomeDeviceHandlers.remove(childHandler);
288 super.childHandlerDisposed(childHandler, childThing);
292 public void dispose() {
293 AccountServlet accountServlet = this.accountServlet;
294 if (accountServlet != null) {
295 accountServlet.dispose();
297 this.accountServlet = null;
302 private void cleanup() {
303 logger.debug("cleanup {}", getThing().getUID().getAsString());
304 ScheduledFuture<?> updateSmartHomeStateJob = this.updateSmartHomeStateJob;
305 if (updateSmartHomeStateJob != null) {
306 updateSmartHomeStateJob.cancel(true);
307 this.updateSmartHomeStateJob = null;
309 ScheduledFuture<?> refreshJob = this.checkDataJob;
310 if (refreshJob != null) {
311 refreshJob.cancel(true);
312 this.checkDataJob = null;
314 ScheduledFuture<?> refreshLogin = this.checkLoginJob;
315 if (refreshLogin != null) {
316 refreshLogin.cancel(true);
317 this.checkLoginJob = null;
319 ScheduledFuture<?> foceCheckDataJob = this.forceCheckDataJob;
320 if (foceCheckDataJob != null) {
321 foceCheckDataJob.cancel(true);
322 this.forceCheckDataJob = null;
324 ScheduledFuture<?> refreshAfterCommandJob = this.refreshAfterCommandJob;
325 if (refreshAfterCommandJob != null) {
326 refreshAfterCommandJob.cancel(true);
327 this.refreshAfterCommandJob = null;
329 ScheduledFuture<?> refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob;
330 if (refreshSmartHomeAfterCommandJob != null) {
331 refreshSmartHomeAfterCommandJob.cancel(true);
332 this.refreshSmartHomeAfterCommandJob = null;
334 Connection connection = this.connection;
335 if (connection != null) {
337 this.connection = null;
339 closeWebSocketConnection();
342 private void checkLogin() {
344 ThingUID uid = getThing().getUID();
345 logger.debug("check login {}", uid.getAsString());
347 synchronized (synchronizeConnection) {
348 Connection currentConnection = this.connection;
349 if (currentConnection == null) {
354 if (currentConnection.getIsLoggedIn()) {
355 if (currentConnection.checkRenewSession()) {
356 setConnection(currentConnection);
359 // read session data from property
360 String sessionStore = this.stateStorage.get("sessionStorage");
362 // try use the session data
363 if (currentConnection.tryRestoreLogin(sessionStore, null)) {
364 setConnection(currentConnection);
367 if (!currentConnection.getIsLoggedIn()) {
368 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
369 "Please login in through web site: http(s)://<YOUROPENHAB>:<YOURPORT>/amazonechocontrol/"
370 + URLEncoder.encode(uid.getId(), "UTF8"));
372 } catch (ConnectionException e) {
373 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
374 } catch (HttpException e) {
375 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
376 } catch (UnknownHostException e) {
377 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
378 "Unknown host name '" + e.getMessage() + "'. Maybe your internet connection is offline");
379 } catch (IOException e) {
380 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
381 } catch (URISyntaxException e) {
382 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
385 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
386 logger.error("check login fails with unexpected error", e);
390 // used to set a valid connection from the web proxy login
391 public void setConnection(@Nullable Connection connection) {
392 this.connection = connection;
393 if (connection != null) {
394 String serializedStorage = connection.serializeLoginData();
395 this.stateStorage.put("sessionStorage", serializedStorage);
397 this.stateStorage.put("sessionStorage", null);
398 updateStatus(ThingStatus.OFFLINE);
400 closeWebSocketConnection();
401 if (connection != null) {
403 updateSmartHomeDeviceList(false);
404 updateFlashBriefingHandlers();
405 updateStatus(ThingStatus.ONLINE);
411 void closeWebSocketConnection() {
412 WebSocketConnection webSocketConnection = this.webSocketConnection;
413 this.webSocketConnection = null;
414 if (webSocketConnection != null) {
415 webSocketConnection.close();
419 private boolean checkWebSocketConnection() {
420 WebSocketConnection webSocketConnection = this.webSocketConnection;
421 if (webSocketConnection == null || webSocketConnection.isClosed()) {
422 Connection connection = this.connection;
423 if (connection != null && connection.getIsLoggedIn()) {
425 this.webSocketConnection = new WebSocketConnection(connection.getAmazonSite(),
426 connection.getSessionCookies(), this);
427 } catch (IOException e) {
428 logger.warn("Web socket connection starting failed", e);
436 private void checkData() {
437 synchronized (synchronizeConnection) {
439 Connection connection = this.connection;
440 if (connection != null && connection.getIsLoggedIn()) {
442 if (checkDataCounter > 60 || forceCheckDataJob != null) {
443 checkDataCounter = 0;
444 forceCheckDataJob = null;
446 if (!checkWebSocketConnection() || checkDataCounter == 0) {
450 logger.debug("checkData {} finished", getThing().getUID().getAsString());
451 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
452 logger.debug("checkData fails", e);
453 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
454 logger.error("checkData fails with unexpected error", e);
459 private void refreshNotifications(@Nullable JsonCommandPayloadPushNotificationChange pushPayload) {
460 Connection currentConnection = this.connection;
461 if (currentConnection == null) {
464 if (!currentConnection.getIsLoggedIn()) {
468 ZonedDateTime timeStamp = ZonedDateTime.now();
470 List<JsonNotificationResponse> notifications = currentConnection.notifications();
471 ZonedDateTime timeStampNow = ZonedDateTime.now();
472 echoHandlers.forEach(echoHandler -> echoHandler.updateNotifications(timeStamp, timeStampNow, pushPayload,
474 } catch (IOException | URISyntaxException | InterruptedException e) {
475 logger.debug("refreshNotifications failed", e);
480 private void refreshData() {
481 synchronized (synchronizeConnection) {
483 logger.debug("refreshing data {}", getThing().getUID().getAsString());
485 // check if logged in
486 Connection currentConnection = null;
487 currentConnection = connection;
488 if (currentConnection != null) {
489 if (!currentConnection.getIsLoggedIn()) {
493 if (currentConnection == null) {
497 // get all devices registered in the account
499 updateSmartHomeDeviceList(false);
500 updateFlashBriefingHandlers();
502 List<DeviceNotificationState> deviceNotificationStates = List.of();
503 List<AscendingAlarmModel> ascendingAlarmModels = List.of();
504 JsonBluetoothStates states = null;
505 List<JsonMusicProvider> musicProviders = null;
506 if (currentConnection.getIsLoggedIn()) {
507 // update notification states
508 deviceNotificationStates = currentConnection.getDeviceNotificationStates();
510 // update ascending alarm
511 ascendingAlarmModels = currentConnection.getAscendingAlarm();
513 // update bluetooth states
514 states = currentConnection.getBluetoothConnectionStates();
516 // update music providers
517 if (currentConnection.getIsLoggedIn()) {
519 musicProviders = currentConnection.getMusicProviders();
520 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
521 logger.debug("Update music provider failed", e);
525 // forward device information to echo handler
526 for (EchoHandler child : echoHandlers) {
527 Device device = findDeviceJson(child.findSerialNumber());
529 List<JsonNotificationSound> notificationSounds = List.of();
530 JsonPlaylists playlists = null;
531 if (device != null && currentConnection.getIsLoggedIn()) {
532 // update notification sounds
534 notificationSounds = currentConnection.getNotificationSounds(device);
535 } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
536 logger.debug("Update notification sounds failed", e);
540 playlists = currentConnection.getPlaylists(device);
541 } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
542 logger.debug("Update playlist failed", e);
546 BluetoothState state = null;
547 if (states != null) {
548 state = states.findStateByDevice(device);
550 DeviceNotificationState deviceNotificationState = null;
551 AscendingAlarmModel ascendingAlarmModel = null;
552 if (device != null) {
553 final String serialNumber = device.serialNumber;
554 if (serialNumber != null) {
555 ascendingAlarmModel = ascendingAlarmModels.stream()
556 .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst()
558 deviceNotificationState = deviceNotificationStates.stream()
559 .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst()
563 child.updateState(this, device, state, deviceNotificationState, ascendingAlarmModel, playlists,
564 notificationSounds, musicProviders);
567 // refresh notifications
568 refreshNotifications(null);
570 // update account state
571 updateStatus(ThingStatus.ONLINE);
573 logger.debug("refresh data {} finished", getThing().getUID().getAsString());
574 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
575 logger.debug("refresh data fails", e);
576 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
577 logger.error("refresh data fails with unexpected error", e);
582 public @Nullable Device findDeviceJson(@Nullable String serialNumber) {
583 if (serialNumber == null || serialNumber.isEmpty()) {
586 return this.jsonSerialNumberDeviceMapping.get(serialNumber);
589 public @Nullable Device findDeviceJsonBySerialOrName(@Nullable String serialOrName) {
590 if (serialOrName == null || serialOrName.isEmpty()) {
594 return this.jsonSerialNumberDeviceMapping.values().stream().filter(
595 d -> serialOrName.equalsIgnoreCase(d.serialNumber) || serialOrName.equalsIgnoreCase(d.accountName))
596 .findFirst().orElse(null);
599 public List<Device> updateDeviceList() {
600 Connection currentConnection = connection;
601 if (currentConnection == null) {
602 return new ArrayList<>();
605 List<Device> devices = null;
607 if (currentConnection.getIsLoggedIn()) {
608 devices = currentConnection.getDeviceList();
610 } catch (IOException | URISyntaxException | InterruptedException e) {
611 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
613 if (devices != null) {
614 // create new device map
615 jsonSerialNumberDeviceMapping = devices.stream().filter(device -> device.serialNumber != null)
616 .collect(Collectors.toMap(d -> Objects.requireNonNull(d.serialNumber), d -> d));
619 List<WakeWord> wakeWords = currentConnection.getWakeWords();
621 for (EchoHandler echoHandler : echoHandlers) {
622 String serialNumber = echoHandler.findSerialNumber();
623 String deviceWakeWord = wakeWords.stream()
624 .filter(wakeWord -> serialNumber.equals(wakeWord.deviceSerialNumber)).findFirst()
625 .map(wakeWord -> wakeWord.wakeWord).orElse(null);
626 echoHandler.setDeviceAndUpdateThingState(this, findDeviceJson(serialNumber), deviceWakeWord);
629 if (devices != null) {
635 public void setEnabledFlashBriefingsJson(String flashBriefingJson) {
636 Connection currentConnection = connection;
637 JsonFeed[] feeds = gson.fromJson(flashBriefingJson, JsonFeed[].class);
638 if (currentConnection != null && feeds != null) {
640 currentConnection.setEnabledFlashBriefings(Arrays.asList(feeds));
641 } catch (IOException | URISyntaxException | InterruptedException e) {
642 logger.warn("Set flashbriefing profile failed", e);
645 updateFlashBriefingHandlers();
648 public String getNewCurrentFlashbriefingConfiguration() {
649 return updateFlashBriefingHandlers();
652 public String updateFlashBriefingHandlers() {
653 Connection currentConnection = connection;
654 if (currentConnection != null) {
655 return updateFlashBriefingHandlers(currentConnection);
660 private String updateFlashBriefingHandlers(Connection currentConnection) {
661 if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) {
662 updateFlashBriefingProfiles(currentConnection);
664 boolean flashBriefingProfileFound = false;
665 for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) {
666 flashBriefingProfileFound |= child.initialize(this, currentFlashBriefingJson);
668 if (flashBriefingProfileFound) {
671 return this.currentFlashBriefingJson;
674 public @Nullable Connection findConnection() {
675 return this.connection;
678 public String getEnabledFlashBriefingsJson() {
679 Connection currentConnection = this.connection;
680 if (currentConnection == null) {
683 updateFlashBriefingProfiles(currentConnection);
684 return this.currentFlashBriefingJson;
687 private void updateFlashBriefingProfiles(Connection currentConnection) {
689 // Make a copy and remove changeable parts
690 JsonFeed[] forSerializer = currentConnection.getEnabledFlashBriefings().stream()
691 .map(source -> new JsonFeed(source.feedId, source.skillId)).toArray(JsonFeed[]::new);
692 this.currentFlashBriefingJson = gson.toJson(forSerializer);
693 } catch (HttpException | JsonSyntaxException | IOException | URISyntaxException | ConnectionException
694 | InterruptedException e) {
695 logger.warn("get flash briefing profiles fails", e);
700 public void webSocketCommandReceived(JsonPushCommand pushCommand) {
702 handleWebsocketCommand(pushCommand);
703 } catch (Exception e) {
704 // should never happen, but if the exception is going out of this function, the binding stop working.
705 logger.warn("handling of websockets fails", e);
709 void handleWebsocketCommand(JsonPushCommand pushCommand) {
710 String command = pushCommand.command;
711 if (command != null) {
712 ScheduledFuture<?> refreshDataDelayed = this.refreshAfterCommandJob;
714 case "PUSH_ACTIVITY":
715 handlePushActivity(pushCommand.payload);
717 case "PUSH_DOPPLER_CONNECTION_CHANGE":
718 case "PUSH_BLUETOOTH_STATE_CHANGE":
719 if (refreshDataDelayed != null) {
720 refreshDataDelayed.cancel(false);
722 this.refreshAfterCommandJob = scheduler.schedule(this::refreshAfterCommand, 700,
723 TimeUnit.MILLISECONDS);
725 case "PUSH_NOTIFICATION_CHANGE":
726 JsonCommandPayloadPushNotificationChange pushPayload = gson.fromJson(pushCommand.payload,
727 JsonCommandPayloadPushNotificationChange.class);
728 refreshNotifications(pushPayload);
731 String payload = pushCommand.payload;
732 if (payload != null && payload.startsWith("{") && payload.endsWith("}")) {
733 JsonCommandPayloadPushDevice devicePayload = Objects
734 .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushDevice.class));
735 DopplerId dopplerId = devicePayload.dopplerId;
736 if (dopplerId != null) {
737 handlePushDeviceCommand(dopplerId, command, payload);
745 private void handlePushDeviceCommand(DopplerId dopplerId, String command, String payload) {
746 EchoHandler echoHandler = findEchoHandlerBySerialNumber(dopplerId.deviceSerialNumber);
747 if (echoHandler != null) {
748 echoHandler.handlePushCommand(command, payload);
752 private void handlePushActivity(@Nullable String payload) {
753 if (payload == null) {
756 JsonCommandPayloadPushActivity pushActivity = Objects
757 .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushActivity.class));
759 Key key = pushActivity.key;
764 Connection connection = this.connection;
765 if (connection == null || !connection.getIsLoggedIn()) {
769 String search = key.registeredUserId + "#" + key.entryId;
770 connection.getActivities(10, pushActivity.timestamp).stream().filter(activity -> search.equals(activity.id))
772 .ifPresent(currentActivity -> currentActivity.getSourceDeviceIds().stream()
773 .map(sourceDeviceId -> findEchoHandlerBySerialNumber(sourceDeviceId.serialNumber))
774 .filter(Objects::nonNull).forEach(echoHandler -> Objects.requireNonNull(echoHandler)
775 .handlePushActivity(currentActivity)));
778 void refreshAfterCommand() {
782 private @Nullable SmartHomeBaseDevice findSmartDeviceHomeJson(SmartHomeDeviceHandler handler) {
783 String id = handler.getId();
785 return jsonIdSmartHomeDeviceMapping.get(id);
790 public int getSmartHomeDevicesDiscoveryMode() {
791 return handlerConfig.discoverSmartHome;
794 public List<SmartHomeBaseDevice> updateSmartHomeDeviceList(boolean forceUpdate) {
795 Connection currentConnection = connection;
796 if (currentConnection == null) {
797 return Collections.emptyList();
800 if (!forceUpdate && smartHomeDeviceHandlers.isEmpty() && getSmartHomeDevicesDiscoveryMode() == 0) {
801 return Collections.emptyList();
804 List<SmartHomeBaseDevice> smartHomeDevices = null;
806 if (currentConnection.getIsLoggedIn()) {
807 smartHomeDevices = currentConnection.getSmarthomeDeviceList();
809 } catch (IOException | URISyntaxException | InterruptedException e) {
810 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
812 if (smartHomeDevices != null) {
814 Map<String, SmartHomeBaseDevice> newJsonIdSmartHomeDeviceMapping = new HashMap<>();
815 for (Object smartHomeDevice : smartHomeDevices) {
816 if (smartHomeDevice instanceof SmartHomeBaseDevice) {
817 SmartHomeBaseDevice smartHomeBaseDevice = (SmartHomeBaseDevice) smartHomeDevice;
818 String id = smartHomeBaseDevice.findId();
820 newJsonIdSmartHomeDeviceMapping.put(id, smartHomeBaseDevice);
824 jsonIdSmartHomeDeviceMapping = newJsonIdSmartHomeDeviceMapping;
827 smartHomeDeviceHandlers
828 .forEach(child -> child.setDeviceAndUpdateThingState(this, findSmartDeviceHomeJson(child)));
830 if (smartHomeDevices != null) {
831 return smartHomeDevices;
834 return Collections.emptyList();
837 public void forceDelayedSmartHomeStateUpdate(@Nullable String deviceId) {
838 if (deviceId == null) {
841 synchronized (synchronizeSmartHomeJobScheduler) {
842 requestedDeviceUpdates.add(deviceId);
843 ScheduledFuture<?> refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob;
844 if (refreshSmartHomeAfterCommandJob != null) {
845 refreshSmartHomeAfterCommandJob.cancel(false);
847 this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 500,
848 TimeUnit.MILLISECONDS);
852 private void updateSmartHomeStateJob() {
853 Set<String> deviceUpdates = new HashSet<>();
855 synchronized (synchronizeSmartHomeJobScheduler) {
856 Connection connection = this.connection;
857 if (connection == null || !connection.getIsLoggedIn()) {
858 this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 1000,
859 TimeUnit.MILLISECONDS);
862 requestedDeviceUpdates.drainTo(deviceUpdates);
863 this.refreshSmartHomeAfterCommandJob = null;
866 deviceUpdates.forEach(this::updateSmartHomeState);
869 private synchronized void updateSmartHomeState(@Nullable String deviceFilterId) {
871 logger.debug("updateSmartHomeState started with deviceFilterId={}", deviceFilterId);
872 Connection connection = this.connection;
873 if (connection == null || !connection.getIsLoggedIn()) {
876 List<SmartHomeBaseDevice> allDevices = getLastKnownSmartHomeDevices();
877 Set<SmartHomeBaseDevice> targetDevices = new HashSet<>();
878 if (deviceFilterId != null) {
879 allDevices.stream().filter(d -> deviceFilterId.equals(d.findId())).findFirst()
880 .ifPresent(targetDevices::add);
882 SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator = this.smartHomeDeviceStateGroupUpdateCalculator;
883 if (smartHomeDeviceStateGroupUpdateCalculator == null) {
886 if (smartHomeDeviceHandlers.isEmpty()) {
889 List<SmartHomeDevice> devicesToUpdate = new ArrayList<>();
890 for (SmartHomeDeviceHandler device : smartHomeDeviceHandlers) {
891 String id = device.getId();
892 SmartHomeBaseDevice baseDevice = jsonIdSmartHomeDeviceMapping.get(id);
893 SmartHomeDeviceHandler.getSupportedSmartHomeDevices(baseDevice, allDevices)
894 .forEach(devicesToUpdate::add);
896 smartHomeDeviceStateGroupUpdateCalculator.removeDevicesWithNoUpdate(devicesToUpdate);
897 devicesToUpdate.stream().filter(Objects::nonNull).forEach(targetDevices::add);
898 if (targetDevices.isEmpty()) {
902 Map<String, JsonArray> applianceIdToCapabilityStates = connection
903 .getSmartHomeDeviceStatesJson(targetDevices);
905 for (SmartHomeDeviceHandler smartHomeDeviceHandler : smartHomeDeviceHandlers) {
906 String id = smartHomeDeviceHandler.getId();
907 if (requestedDeviceUpdates.contains(id)) {
908 logger.debug("Device update {} suspended", id);
911 if (deviceFilterId == null || id.equals(deviceFilterId)) {
912 smartHomeDeviceHandler.updateChannelStates(allDevices, applianceIdToCapabilityStates);
914 logger.trace("Id {} not matching filter {}", id, deviceFilterId);
918 logger.debug("updateSmartHomeState finished");
919 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
920 logger.debug("updateSmartHomeState fails", e);
921 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
922 logger.warn("updateSmartHomeState fails with unexpected error", e);