2 * Copyright (c) 2010-2023 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.JsonAscendingAlarm.AscendingAlarmModel;
48 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates;
49 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState;
50 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushActivity;
51 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushActivity.Key;
52 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushDevice;
53 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushDevice.DopplerId;
54 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushNotificationChange;
55 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState.DeviceNotificationState;
56 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device;
57 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed;
58 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider;
59 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse;
60 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound;
61 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists;
62 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPushCommand;
63 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice;
64 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonWakeWords.WakeWord;
65 import org.openhab.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice;
66 import org.openhab.binding.amazonechocontrol.internal.smarthome.SmartHomeDeviceStateGroupUpdateCalculator;
67 import org.openhab.core.storage.Storage;
68 import org.openhab.core.thing.Bridge;
69 import org.openhab.core.thing.ChannelUID;
70 import org.openhab.core.thing.Thing;
71 import org.openhab.core.thing.ThingStatus;
72 import org.openhab.core.thing.ThingStatusDetail;
73 import org.openhab.core.thing.ThingUID;
74 import org.openhab.core.thing.binding.BaseBridgeHandler;
75 import org.openhab.core.thing.binding.ThingHandler;
76 import org.openhab.core.types.Command;
77 import org.openhab.core.types.RefreshType;
78 import org.openhab.core.types.State;
79 import org.osgi.service.http.HttpService;
80 import org.slf4j.Logger;
81 import org.slf4j.LoggerFactory;
83 import com.google.gson.Gson;
84 import com.google.gson.JsonArray;
85 import com.google.gson.JsonSyntaxException;
88 * Handles the connection to the amazon server.
90 * @author Michael Geramb - Initial Contribution
93 public class AccountHandler extends BaseBridgeHandler implements IWebSocketCommandHandler, IAmazonThingHandler {
94 private final Logger logger = LoggerFactory.getLogger(AccountHandler.class);
95 private final Storage<String> stateStorage;
96 private @Nullable Connection connection;
97 private @Nullable WebSocketConnection webSocketConnection;
99 private final Set<EchoHandler> echoHandlers = new CopyOnWriteArraySet<>();
100 private final Set<SmartHomeDeviceHandler> smartHomeDeviceHandlers = new CopyOnWriteArraySet<>();
101 private final Set<FlashBriefingProfileHandler> flashBriefingProfileHandlers = new CopyOnWriteArraySet<>();
103 private final Object synchronizeConnection = new Object();
104 private Map<String, Device> jsonSerialNumberDeviceMapping = new HashMap<>();
105 private Map<String, SmartHomeBaseDevice> jsonIdSmartHomeDeviceMapping = new HashMap<>();
107 private @Nullable ScheduledFuture<?> checkDataJob;
108 private @Nullable ScheduledFuture<?> checkLoginJob;
109 private @Nullable ScheduledFuture<?> updateSmartHomeStateJob;
110 private @Nullable ScheduledFuture<?> refreshAfterCommandJob;
111 private @Nullable ScheduledFuture<?> refreshSmartHomeAfterCommandJob;
112 private final Object synchronizeSmartHomeJobScheduler = new Object();
113 private @Nullable ScheduledFuture<?> forceCheckDataJob;
114 private String currentFlashBriefingJson = "";
115 private final HttpService httpService;
116 private @Nullable AccountServlet accountServlet;
117 private final Gson gson;
118 private int checkDataCounter;
119 private final LinkedBlockingQueue<String> requestedDeviceUpdates = new LinkedBlockingQueue<>();
120 private @Nullable SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator;
121 private List<ChannelHandler> channelHandlers = new ArrayList<>();
123 private AccountHandlerConfig handlerConfig = new AccountHandlerConfig();
125 public AccountHandler(Bridge bridge, HttpService httpService, Storage<String> stateStorage, Gson gson) {
128 this.httpService = httpService;
129 this.stateStorage = stateStorage;
130 channelHandlers.add(new ChannelHandlerSendMessage(this, this.gson));
134 public void initialize() {
135 handlerConfig = getConfig().as(AccountHandlerConfig.class);
137 synchronized (synchronizeConnection) {
138 Connection connection = this.connection;
139 if (connection == null) {
140 this.connection = new Connection(null, gson);
144 if (accountServlet == null) {
146 accountServlet = new AccountServlet(httpService, this.getThing().getUID().getId(), this, gson);
147 } catch (IllegalStateException e) {
148 logger.warn("Failed to create account servlet", e);
152 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login");
154 checkLoginJob = scheduler.scheduleWithFixedDelay(this::checkLogin, 0, 60, TimeUnit.SECONDS);
155 checkDataJob = scheduler.scheduleWithFixedDelay(this::checkData, 4, 60, TimeUnit.SECONDS);
157 int pollingIntervalAlexa = handlerConfig.pollingIntervalSmartHomeAlexa;
158 if (pollingIntervalAlexa < 10) {
159 pollingIntervalAlexa = 10;
161 int pollingIntervalSkills = handlerConfig.pollingIntervalSmartSkills;
162 if (pollingIntervalSkills < 60) {
163 pollingIntervalSkills = 60;
165 smartHomeDeviceStateGroupUpdateCalculator = new SmartHomeDeviceStateGroupUpdateCalculator(pollingIntervalAlexa,
166 pollingIntervalSkills);
167 updateSmartHomeStateJob = scheduler.scheduleWithFixedDelay(() -> updateSmartHomeState(null), 20, 10,
172 public void updateChannelState(String channelId, State state) {
173 updateState(channelId, state);
177 public void handleCommand(ChannelUID channelUID, Command command) {
179 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
180 Connection connection = this.connection;
181 if (connection == null) {
185 String channelId = channelUID.getId();
186 for (ChannelHandler channelHandler : channelHandlers) {
187 if (channelHandler.tryHandleCommand(new Device(), connection, channelId, command)) {
191 if (command instanceof RefreshType) {
194 } catch (IOException | URISyntaxException | InterruptedException e) {
195 logger.info("handleCommand fails", e);
200 public void startAnnouncement(Device device, String speak, String bodyText, @Nullable String title,
201 @Nullable Integer volume) throws IOException, URISyntaxException {
202 EchoHandler echoHandler = findEchoHandlerBySerialNumber(device.serialNumber);
203 if (echoHandler != null) {
204 echoHandler.startAnnouncement(device, speak, bodyText, title, volume);
208 public List<FlashBriefingProfileHandler> getFlashBriefingProfileHandlers() {
209 return new ArrayList<>(flashBriefingProfileHandlers);
212 public List<Device> getLastKnownDevices() {
213 return new ArrayList<>(jsonSerialNumberDeviceMapping.values());
216 public List<SmartHomeBaseDevice> getLastKnownSmartHomeDevices() {
217 return new ArrayList<>(jsonIdSmartHomeDeviceMapping.values());
220 public void addEchoHandler(EchoHandler echoHandler) {
221 if (echoHandlers.add(echoHandler)) {
226 public void addSmartHomeDeviceHandler(SmartHomeDeviceHandler smartHomeDeviceHandler) {
227 if (smartHomeDeviceHandlers.add(smartHomeDeviceHandler)) {
232 public void forceCheckData() {
233 if (forceCheckDataJob == null) {
234 forceCheckDataJob = scheduler.schedule(this::checkData, 1000, TimeUnit.MILLISECONDS);
238 public @Nullable Thing findThingBySerialNumber(@Nullable String deviceSerialNumber) {
239 EchoHandler echoHandler = findEchoHandlerBySerialNumber(deviceSerialNumber);
240 if (echoHandler != null) {
241 return echoHandler.getThing();
246 public @Nullable EchoHandler findEchoHandlerBySerialNumber(@Nullable String deviceSerialNumber) {
247 for (EchoHandler echoHandler : echoHandlers) {
248 if (deviceSerialNumber != null && deviceSerialNumber.equals(echoHandler.findSerialNumber())) {
255 public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBriefingProfileHandler) {
256 flashBriefingProfileHandlers.add(flashBriefingProfileHandler);
257 Connection connection = this.connection;
258 if (connection != null && connection.getIsLoggedIn()) {
259 if (currentFlashBriefingJson.isEmpty()) {
260 updateFlashBriefingProfiles(connection);
262 flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson);
266 private void scheduleUpdate() {
267 checkDataCounter = 999;
271 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
272 super.childHandlerInitialized(childHandler, childThing);
277 public void handleRemoval() {
279 super.handleRemoval();
283 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
284 // check for echo handler
285 if (childHandler instanceof EchoHandler) {
286 echoHandlers.remove(childHandler);
288 // check for flash briefing profile handler
289 if (childHandler instanceof FlashBriefingProfileHandler) {
290 flashBriefingProfileHandlers.remove(childHandler);
292 // check for flash briefing profile handler
293 if (childHandler instanceof SmartHomeDeviceHandler) {
294 smartHomeDeviceHandlers.remove(childHandler);
296 super.childHandlerDisposed(childHandler, childThing);
300 public void dispose() {
301 AccountServlet accountServlet = this.accountServlet;
302 if (accountServlet != null) {
303 accountServlet.dispose();
305 this.accountServlet = null;
310 private void cleanup() {
311 logger.debug("cleanup {}", getThing().getUID().getAsString());
312 ScheduledFuture<?> updateSmartHomeStateJob = this.updateSmartHomeStateJob;
313 if (updateSmartHomeStateJob != null) {
314 updateSmartHomeStateJob.cancel(true);
315 this.updateSmartHomeStateJob = null;
317 ScheduledFuture<?> refreshJob = this.checkDataJob;
318 if (refreshJob != null) {
319 refreshJob.cancel(true);
320 this.checkDataJob = null;
322 ScheduledFuture<?> refreshLogin = this.checkLoginJob;
323 if (refreshLogin != null) {
324 refreshLogin.cancel(true);
325 this.checkLoginJob = null;
327 ScheduledFuture<?> foceCheckDataJob = this.forceCheckDataJob;
328 if (foceCheckDataJob != null) {
329 foceCheckDataJob.cancel(true);
330 this.forceCheckDataJob = null;
332 ScheduledFuture<?> refreshAfterCommandJob = this.refreshAfterCommandJob;
333 if (refreshAfterCommandJob != null) {
334 refreshAfterCommandJob.cancel(true);
335 this.refreshAfterCommandJob = null;
337 ScheduledFuture<?> refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob;
338 if (refreshSmartHomeAfterCommandJob != null) {
339 refreshSmartHomeAfterCommandJob.cancel(true);
340 this.refreshSmartHomeAfterCommandJob = null;
342 Connection connection = this.connection;
343 if (connection != null) {
345 this.connection = null;
347 closeWebSocketConnection();
350 private void checkLogin() {
352 ThingUID uid = getThing().getUID();
353 logger.debug("check login {}", uid.getAsString());
355 synchronized (synchronizeConnection) {
356 Connection currentConnection = this.connection;
357 if (currentConnection == null) {
362 if (currentConnection.getIsLoggedIn()) {
363 if (currentConnection.checkRenewSession()) {
364 setConnection(currentConnection);
367 // read session data from property
368 String sessionStore = this.stateStorage.get("sessionStorage");
370 // try use the session data
371 if (currentConnection.tryRestoreLogin(sessionStore, null)) {
372 setConnection(currentConnection);
375 if (!currentConnection.getIsLoggedIn()) {
376 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
377 "Please login in through web site: http(s)://<YOUROPENHAB>:<YOURPORT>/amazonechocontrol/"
378 + URLEncoder.encode(uid.getId(), "UTF8"));
380 } catch (ConnectionException e) {
381 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
382 } catch (HttpException e) {
383 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
384 } catch (UnknownHostException e) {
385 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
386 "Unknown host name '" + e.getMessage() + "'. Maybe your internet connection is offline");
387 } catch (IOException e) {
388 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
389 } catch (URISyntaxException e) {
390 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
393 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
394 logger.error("check login fails with unexpected error", e);
398 // used to set a valid connection from the web proxy login
399 public void setConnection(@Nullable Connection connection) {
400 this.connection = connection;
401 if (connection != null) {
402 String serializedStorage = connection.serializeLoginData();
403 this.stateStorage.put("sessionStorage", serializedStorage);
405 this.stateStorage.put("sessionStorage", null);
406 updateStatus(ThingStatus.OFFLINE);
408 closeWebSocketConnection();
409 if (connection != null) {
411 updateSmartHomeDeviceList(false);
412 updateFlashBriefingHandlers();
413 updateStatus(ThingStatus.ONLINE);
419 void closeWebSocketConnection() {
420 WebSocketConnection webSocketConnection = this.webSocketConnection;
421 this.webSocketConnection = null;
422 if (webSocketConnection != null) {
423 webSocketConnection.close();
427 private boolean checkWebSocketConnection() {
428 WebSocketConnection webSocketConnection = this.webSocketConnection;
429 if (webSocketConnection == null || webSocketConnection.isClosed()) {
430 Connection connection = this.connection;
431 if (connection != null && connection.getIsLoggedIn()) {
433 this.webSocketConnection = new WebSocketConnection(connection.getAmazonSite(),
434 connection.getSessionCookies(), this);
435 } catch (IOException e) {
436 logger.warn("Web socket connection starting failed", e);
444 private void checkData() {
445 synchronized (synchronizeConnection) {
447 Connection connection = this.connection;
448 if (connection != null && connection.getIsLoggedIn()) {
450 if (checkDataCounter > 60 || forceCheckDataJob != null) {
451 checkDataCounter = 0;
452 forceCheckDataJob = null;
454 if (!checkWebSocketConnection() || checkDataCounter == 0) {
458 logger.debug("checkData {} finished", getThing().getUID().getAsString());
459 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
460 logger.debug("checkData fails", e);
461 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
462 logger.error("checkData fails with unexpected error", e);
467 private void refreshNotifications(@Nullable JsonCommandPayloadPushNotificationChange pushPayload) {
468 Connection currentConnection = this.connection;
469 if (currentConnection == null) {
472 if (!currentConnection.getIsLoggedIn()) {
476 ZonedDateTime timeStamp = ZonedDateTime.now();
478 List<JsonNotificationResponse> notifications = currentConnection.notifications();
479 ZonedDateTime timeStampNow = ZonedDateTime.now();
480 echoHandlers.forEach(echoHandler -> echoHandler.updateNotifications(timeStamp, timeStampNow, pushPayload,
482 } catch (IOException | URISyntaxException | InterruptedException e) {
483 logger.debug("refreshNotifications failed", e);
488 private void refreshData() {
489 synchronized (synchronizeConnection) {
491 logger.debug("refreshing data {}", getThing().getUID().getAsString());
493 // check if logged in
494 Connection currentConnection = null;
495 currentConnection = connection;
496 if (currentConnection != null) {
497 if (!currentConnection.getIsLoggedIn()) {
501 if (currentConnection == null) {
505 // get all devices registered in the account
507 updateSmartHomeDeviceList(false);
508 updateFlashBriefingHandlers();
510 List<DeviceNotificationState> deviceNotificationStates = List.of();
511 List<AscendingAlarmModel> ascendingAlarmModels = List.of();
512 JsonBluetoothStates states = null;
513 List<JsonMusicProvider> musicProviders = null;
514 if (currentConnection.getIsLoggedIn()) {
515 // update notification states
516 deviceNotificationStates = currentConnection.getDeviceNotificationStates();
518 // update ascending alarm
519 ascendingAlarmModels = currentConnection.getAscendingAlarm();
521 // update bluetooth states
522 states = currentConnection.getBluetoothConnectionStates();
524 // update music providers
525 if (currentConnection.getIsLoggedIn()) {
527 musicProviders = currentConnection.getMusicProviders();
528 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
529 logger.debug("Update music provider failed", e);
533 // forward device information to echo handler
534 for (EchoHandler child : echoHandlers) {
535 Device device = findDeviceJson(child.findSerialNumber());
537 List<JsonNotificationSound> notificationSounds = List.of();
538 JsonPlaylists playlists = null;
539 if (device != null && currentConnection.getIsLoggedIn()) {
540 // update notification sounds
542 notificationSounds = currentConnection.getNotificationSounds(device);
543 } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
544 logger.debug("Update notification sounds failed", e);
548 playlists = currentConnection.getPlaylists(device);
549 } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
550 logger.debug("Update playlist failed", e);
554 BluetoothState state = null;
555 if (states != null) {
556 state = states.findStateByDevice(device);
558 DeviceNotificationState deviceNotificationState = null;
559 AscendingAlarmModel ascendingAlarmModel = null;
560 if (device != null) {
561 final String serialNumber = device.serialNumber;
562 if (serialNumber != null) {
563 ascendingAlarmModel = ascendingAlarmModels.stream()
564 .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst()
566 deviceNotificationState = deviceNotificationStates.stream()
567 .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst()
571 child.updateState(this, device, state, deviceNotificationState, ascendingAlarmModel, playlists,
572 notificationSounds, musicProviders);
575 // refresh notifications
576 refreshNotifications(null);
578 // update account state
579 updateStatus(ThingStatus.ONLINE);
581 logger.debug("refresh data {} finished", getThing().getUID().getAsString());
582 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
583 logger.debug("refresh data fails", e);
584 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
585 logger.error("refresh data fails with unexpected error", e);
590 public @Nullable Device findDeviceJson(@Nullable String serialNumber) {
591 if (serialNumber == null || serialNumber.isEmpty()) {
594 return this.jsonSerialNumberDeviceMapping.get(serialNumber);
597 public @Nullable Device findDeviceJsonBySerialOrName(@Nullable String serialOrName) {
598 if (serialOrName == null || serialOrName.isEmpty()) {
602 return this.jsonSerialNumberDeviceMapping.values().stream().filter(
603 d -> serialOrName.equalsIgnoreCase(d.serialNumber) || serialOrName.equalsIgnoreCase(d.accountName))
604 .findFirst().orElse(null);
607 public List<Device> updateDeviceList() {
608 Connection currentConnection = connection;
609 if (currentConnection == null) {
610 return new ArrayList<>();
613 List<Device> devices = null;
615 if (currentConnection.getIsLoggedIn()) {
616 devices = currentConnection.getDeviceList();
618 } catch (IOException | URISyntaxException | InterruptedException e) {
619 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
621 if (devices != null) {
622 // create new device map
623 jsonSerialNumberDeviceMapping = devices.stream().filter(device -> device.serialNumber != null)
624 .collect(Collectors.toMap(d -> Objects.requireNonNull(d.serialNumber), d -> d));
627 List<WakeWord> wakeWords = currentConnection.getWakeWords();
629 for (EchoHandler echoHandler : echoHandlers) {
630 String serialNumber = echoHandler.findSerialNumber();
631 String deviceWakeWord = wakeWords.stream()
632 .filter(wakeWord -> serialNumber.equals(wakeWord.deviceSerialNumber)).findFirst()
633 .map(wakeWord -> wakeWord.wakeWord).orElse(null);
634 echoHandler.setDeviceAndUpdateThingState(this, findDeviceJson(serialNumber), deviceWakeWord);
637 if (devices != null) {
643 public void setEnabledFlashBriefingsJson(String flashBriefingJson) {
644 Connection currentConnection = connection;
645 JsonFeed[] feeds = gson.fromJson(flashBriefingJson, JsonFeed[].class);
646 if (currentConnection != null && feeds != null) {
648 currentConnection.setEnabledFlashBriefings(Arrays.asList(feeds));
649 } catch (IOException | URISyntaxException | InterruptedException e) {
650 logger.warn("Set flashbriefing profile failed", e);
653 updateFlashBriefingHandlers();
656 public String getNewCurrentFlashbriefingConfiguration() {
657 return updateFlashBriefingHandlers();
660 public String updateFlashBriefingHandlers() {
661 Connection currentConnection = connection;
662 if (currentConnection != null) {
663 return updateFlashBriefingHandlers(currentConnection);
668 private String updateFlashBriefingHandlers(Connection currentConnection) {
669 if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) {
670 updateFlashBriefingProfiles(currentConnection);
672 boolean flashBriefingProfileFound = false;
673 for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) {
674 flashBriefingProfileFound |= child.initialize(this, currentFlashBriefingJson);
676 if (flashBriefingProfileFound) {
679 return this.currentFlashBriefingJson;
682 public @Nullable Connection findConnection() {
683 return this.connection;
686 public String getEnabledFlashBriefingsJson() {
687 Connection currentConnection = this.connection;
688 if (currentConnection == null) {
691 updateFlashBriefingProfiles(currentConnection);
692 return this.currentFlashBriefingJson;
695 private void updateFlashBriefingProfiles(Connection currentConnection) {
697 // Make a copy and remove changeable parts
698 JsonFeed[] forSerializer = currentConnection.getEnabledFlashBriefings().stream()
699 .map(source -> new JsonFeed(source.feedId, source.skillId)).toArray(JsonFeed[]::new);
700 this.currentFlashBriefingJson = gson.toJson(forSerializer);
701 } catch (HttpException | JsonSyntaxException | IOException | URISyntaxException | ConnectionException
702 | InterruptedException e) {
703 logger.warn("get flash briefing profiles fails", e);
708 public void webSocketCommandReceived(JsonPushCommand pushCommand) {
710 handleWebsocketCommand(pushCommand);
711 } catch (Exception e) {
712 // should never happen, but if the exception is going out of this function, the binding stop working.
713 logger.warn("handling of websockets fails", e);
717 void handleWebsocketCommand(JsonPushCommand pushCommand) {
718 String command = pushCommand.command;
719 if (command != null) {
720 ScheduledFuture<?> refreshDataDelayed = this.refreshAfterCommandJob;
722 case "PUSH_ACTIVITY":
723 handlePushActivity(pushCommand.payload);
725 case "PUSH_DOPPLER_CONNECTION_CHANGE":
726 case "PUSH_BLUETOOTH_STATE_CHANGE":
727 if (refreshDataDelayed != null) {
728 refreshDataDelayed.cancel(false);
730 this.refreshAfterCommandJob = scheduler.schedule(this::refreshAfterCommand, 700,
731 TimeUnit.MILLISECONDS);
733 case "PUSH_NOTIFICATION_CHANGE":
734 JsonCommandPayloadPushNotificationChange pushPayload = gson.fromJson(pushCommand.payload,
735 JsonCommandPayloadPushNotificationChange.class);
736 refreshNotifications(pushPayload);
739 String payload = pushCommand.payload;
740 if (payload != null && payload.startsWith("{") && payload.endsWith("}")) {
741 JsonCommandPayloadPushDevice devicePayload = Objects
742 .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushDevice.class));
743 DopplerId dopplerId = devicePayload.dopplerId;
744 if (dopplerId != null) {
745 handlePushDeviceCommand(dopplerId, command, payload);
753 private void handlePushDeviceCommand(DopplerId dopplerId, String command, String payload) {
754 EchoHandler echoHandler = findEchoHandlerBySerialNumber(dopplerId.deviceSerialNumber);
755 if (echoHandler != null) {
756 echoHandler.handlePushCommand(command, payload);
760 private void handlePushActivity(@Nullable String payload) {
761 if (payload == null) {
764 JsonCommandPayloadPushActivity pushActivity = Objects
765 .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushActivity.class));
767 Key key = pushActivity.key;
772 Connection connection = this.connection;
773 if (connection == null || !connection.getIsLoggedIn()) {
777 String search = key.registeredUserId + "#" + key.entryId;
778 connection.getActivities(10, pushActivity.timestamp).stream().filter(activity -> search.equals(activity.id))
780 .ifPresent(currentActivity -> currentActivity.getSourceDeviceIds().stream()
781 .map(sourceDeviceId -> findEchoHandlerBySerialNumber(sourceDeviceId.serialNumber))
782 .filter(Objects::nonNull).forEach(echoHandler -> Objects.requireNonNull(echoHandler)
783 .handlePushActivity(currentActivity)));
786 void refreshAfterCommand() {
790 private @Nullable SmartHomeBaseDevice findSmartDeviceHomeJson(SmartHomeDeviceHandler handler) {
791 String id = handler.getId();
793 return jsonIdSmartHomeDeviceMapping.get(id);
798 public int getSmartHomeDevicesDiscoveryMode() {
799 return handlerConfig.discoverSmartHome;
802 public List<SmartHomeBaseDevice> updateSmartHomeDeviceList(boolean forceUpdate) {
803 Connection currentConnection = connection;
804 if (currentConnection == null) {
805 return Collections.emptyList();
808 if (!forceUpdate && smartHomeDeviceHandlers.isEmpty() && getSmartHomeDevicesDiscoveryMode() == 0) {
809 return Collections.emptyList();
812 List<SmartHomeBaseDevice> smartHomeDevices = null;
814 if (currentConnection.getIsLoggedIn()) {
815 smartHomeDevices = currentConnection.getSmarthomeDeviceList();
817 } catch (IOException | URISyntaxException | InterruptedException e) {
818 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
820 if (smartHomeDevices != null) {
822 Map<String, SmartHomeBaseDevice> newJsonIdSmartHomeDeviceMapping = new HashMap<>();
823 for (Object smartHomeDevice : smartHomeDevices) {
824 if (smartHomeDevice instanceof SmartHomeBaseDevice) {
825 SmartHomeBaseDevice smartHomeBaseDevice = (SmartHomeBaseDevice) smartHomeDevice;
826 String id = smartHomeBaseDevice.findId();
828 newJsonIdSmartHomeDeviceMapping.put(id, smartHomeBaseDevice);
832 jsonIdSmartHomeDeviceMapping = newJsonIdSmartHomeDeviceMapping;
835 smartHomeDeviceHandlers
836 .forEach(child -> child.setDeviceAndUpdateThingState(this, findSmartDeviceHomeJson(child)));
838 if (smartHomeDevices != null) {
839 return smartHomeDevices;
842 return Collections.emptyList();
845 public void forceDelayedSmartHomeStateUpdate(@Nullable String deviceId) {
846 if (deviceId == null) {
849 synchronized (synchronizeSmartHomeJobScheduler) {
850 requestedDeviceUpdates.add(deviceId);
851 ScheduledFuture<?> refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob;
852 if (refreshSmartHomeAfterCommandJob != null) {
853 refreshSmartHomeAfterCommandJob.cancel(false);
855 this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 500,
856 TimeUnit.MILLISECONDS);
860 private void updateSmartHomeStateJob() {
861 Set<String> deviceUpdates = new HashSet<>();
863 synchronized (synchronizeSmartHomeJobScheduler) {
864 Connection connection = this.connection;
865 if (connection == null || !connection.getIsLoggedIn()) {
866 this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 1000,
867 TimeUnit.MILLISECONDS);
870 requestedDeviceUpdates.drainTo(deviceUpdates);
871 this.refreshSmartHomeAfterCommandJob = null;
874 deviceUpdates.forEach(this::updateSmartHomeState);
877 private synchronized void updateSmartHomeState(@Nullable String deviceFilterId) {
879 logger.debug("updateSmartHomeState started with deviceFilterId={}", deviceFilterId);
880 Connection connection = this.connection;
881 if (connection == null || !connection.getIsLoggedIn()) {
884 List<SmartHomeBaseDevice> allDevices = getLastKnownSmartHomeDevices();
885 Set<SmartHomeBaseDevice> targetDevices = new HashSet<>();
886 if (deviceFilterId != null) {
887 allDevices.stream().filter(d -> deviceFilterId.equals(d.findId())).findFirst()
888 .ifPresent(targetDevices::add);
890 SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator = this.smartHomeDeviceStateGroupUpdateCalculator;
891 if (smartHomeDeviceStateGroupUpdateCalculator == null) {
894 if (smartHomeDeviceHandlers.isEmpty()) {
897 List<SmartHomeDevice> devicesToUpdate = new ArrayList<>();
898 for (SmartHomeDeviceHandler device : smartHomeDeviceHandlers) {
899 String id = device.getId();
900 SmartHomeBaseDevice baseDevice = jsonIdSmartHomeDeviceMapping.get(id);
901 SmartHomeDeviceHandler.getSupportedSmartHomeDevices(baseDevice, allDevices)
902 .forEach(devicesToUpdate::add);
904 smartHomeDeviceStateGroupUpdateCalculator.removeDevicesWithNoUpdate(devicesToUpdate);
905 devicesToUpdate.stream().filter(Objects::nonNull).forEach(targetDevices::add);
906 if (targetDevices.isEmpty()) {
910 Map<String, JsonArray> applianceIdToCapabilityStates = connection
911 .getSmartHomeDeviceStatesJson(targetDevices);
913 for (SmartHomeDeviceHandler smartHomeDeviceHandler : smartHomeDeviceHandlers) {
914 String id = smartHomeDeviceHandler.getId();
915 if (requestedDeviceUpdates.contains(id)) {
916 logger.debug("Device update {} suspended", id);
919 if (deviceFilterId == null || id.equals(deviceFilterId)) {
920 smartHomeDeviceHandler.updateChannelStates(allDevices, applianceIdToCapabilityStates);
922 logger.trace("Id {} not matching filter {}", id, deviceFilterId);
926 logger.debug("updateSmartHomeState finished");
927 } catch (HttpException | JsonSyntaxException | ConnectionException e) {
928 logger.debug("updateSmartHomeState fails", e);
929 } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
930 logger.warn("updateSmartHomeState fails with unexpected error", e);