]> git.basschouten.com Git - openhab-addons.git/blob
9c773ac4cc8a5db90fb195b8b5a1454012079ca6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.amazonechocontrol.internal.handler;
14
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;
26 import java.util.Map;
27 import java.util.Objects;
28 import java.util.Set;
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;
34
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;
82
83 import com.google.gson.Gson;
84 import com.google.gson.JsonArray;
85 import com.google.gson.JsonSyntaxException;
86
87 /**
88  * Handles the connection to the amazon server.
89  *
90  * @author Michael Geramb - Initial Contribution
91  */
92 @NonNullByDefault
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;
98
99     private final Set<EchoHandler> echoHandlers = new CopyOnWriteArraySet<>();
100     private final Set<SmartHomeDeviceHandler> smartHomeDeviceHandlers = new CopyOnWriteArraySet<>();
101     private final Set<FlashBriefingProfileHandler> flashBriefingProfileHandlers = new CopyOnWriteArraySet<>();
102
103     private final Object synchronizeConnection = new Object();
104     private Map<String, Device> jsonSerialNumberDeviceMapping = new HashMap<>();
105     private Map<String, SmartHomeBaseDevice> jsonIdSmartHomeDeviceMapping = new HashMap<>();
106
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<>();
122
123     private AccountHandlerConfig handlerConfig = new AccountHandlerConfig();
124
125     public AccountHandler(Bridge bridge, HttpService httpService, Storage<String> stateStorage, Gson gson) {
126         super(bridge);
127         this.gson = gson;
128         this.httpService = httpService;
129         this.stateStorage = stateStorage;
130         channelHandlers.add(new ChannelHandlerSendMessage(this, this.gson));
131     }
132
133     @Override
134     public void initialize() {
135         handlerConfig = getConfig().as(AccountHandlerConfig.class);
136
137         synchronized (synchronizeConnection) {
138             Connection connection = this.connection;
139             if (connection == null) {
140                 this.connection = new Connection(null, gson);
141             }
142         }
143
144         if (accountServlet == null) {
145             try {
146                 accountServlet = new AccountServlet(httpService, this.getThing().getUID().getId(), this, gson);
147             } catch (IllegalStateException e) {
148                 logger.warn("Failed to create account servlet", e);
149             }
150         }
151
152         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login");
153
154         checkLoginJob = scheduler.scheduleWithFixedDelay(this::checkLogin, 0, 60, TimeUnit.SECONDS);
155         checkDataJob = scheduler.scheduleWithFixedDelay(this::checkData, 4, 60, TimeUnit.SECONDS);
156
157         int pollingIntervalAlexa = handlerConfig.pollingIntervalSmartHomeAlexa;
158         if (pollingIntervalAlexa < 10) {
159             pollingIntervalAlexa = 10;
160         }
161         int pollingIntervalSkills = handlerConfig.pollingIntervalSmartSkills;
162         if (pollingIntervalSkills < 60) {
163             pollingIntervalSkills = 60;
164         }
165         smartHomeDeviceStateGroupUpdateCalculator = new SmartHomeDeviceStateGroupUpdateCalculator(pollingIntervalAlexa,
166                 pollingIntervalSkills);
167         updateSmartHomeStateJob = scheduler.scheduleWithFixedDelay(() -> updateSmartHomeState(null), 20, 10,
168                 TimeUnit.SECONDS);
169     }
170
171     @Override
172     public void updateChannelState(String channelId, State state) {
173         updateState(channelId, state);
174     }
175
176     @Override
177     public void handleCommand(ChannelUID channelUID, Command command) {
178         try {
179             logger.trace("Command '{}' received for channel '{}'", command, channelUID);
180             Connection connection = this.connection;
181             if (connection == null) {
182                 return;
183             }
184
185             String channelId = channelUID.getId();
186             for (ChannelHandler channelHandler : channelHandlers) {
187                 if (channelHandler.tryHandleCommand(new Device(), connection, channelId, command)) {
188                     return;
189                 }
190             }
191             if (command instanceof RefreshType) {
192                 refreshData();
193             }
194         } catch (IOException | URISyntaxException | InterruptedException e) {
195             logger.info("handleCommand fails", e);
196         }
197     }
198
199     @Override
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);
205         }
206     }
207
208     public List<FlashBriefingProfileHandler> getFlashBriefingProfileHandlers() {
209         return new ArrayList<>(flashBriefingProfileHandlers);
210     }
211
212     public List<Device> getLastKnownDevices() {
213         return new ArrayList<>(jsonSerialNumberDeviceMapping.values());
214     }
215
216     public List<SmartHomeBaseDevice> getLastKnownSmartHomeDevices() {
217         return new ArrayList<>(jsonIdSmartHomeDeviceMapping.values());
218     }
219
220     public void addEchoHandler(EchoHandler echoHandler) {
221         if (echoHandlers.add(echoHandler)) {
222             forceCheckData();
223         }
224     }
225
226     public void addSmartHomeDeviceHandler(SmartHomeDeviceHandler smartHomeDeviceHandler) {
227         if (smartHomeDeviceHandlers.add(smartHomeDeviceHandler)) {
228             forceCheckData();
229         }
230     }
231
232     public void forceCheckData() {
233         if (forceCheckDataJob == null) {
234             forceCheckDataJob = scheduler.schedule(this::checkData, 1000, TimeUnit.MILLISECONDS);
235         }
236     }
237
238     public @Nullable Thing findThingBySerialNumber(@Nullable String deviceSerialNumber) {
239         EchoHandler echoHandler = findEchoHandlerBySerialNumber(deviceSerialNumber);
240         if (echoHandler != null) {
241             return echoHandler.getThing();
242         }
243         return null;
244     }
245
246     public @Nullable EchoHandler findEchoHandlerBySerialNumber(@Nullable String deviceSerialNumber) {
247         for (EchoHandler echoHandler : echoHandlers) {
248             if (deviceSerialNumber != null && deviceSerialNumber.equals(echoHandler.findSerialNumber())) {
249                 return echoHandler;
250             }
251         }
252         return null;
253     }
254
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);
261             }
262             flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson);
263         }
264     }
265
266     private void scheduleUpdate() {
267         checkDataCounter = 999;
268     }
269
270     @Override
271     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
272         super.childHandlerInitialized(childHandler, childThing);
273         scheduleUpdate();
274     }
275
276     @Override
277     public void handleRemoval() {
278         cleanup();
279         super.handleRemoval();
280     }
281
282     @Override
283     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
284         // check for echo handler
285         if (childHandler instanceof EchoHandler) {
286             echoHandlers.remove(childHandler);
287         }
288         // check for flash briefing profile handler
289         if (childHandler instanceof FlashBriefingProfileHandler) {
290             flashBriefingProfileHandlers.remove(childHandler);
291         }
292         // check for flash briefing profile handler
293         if (childHandler instanceof SmartHomeDeviceHandler) {
294             smartHomeDeviceHandlers.remove(childHandler);
295         }
296         super.childHandlerDisposed(childHandler, childThing);
297     }
298
299     @Override
300     public void dispose() {
301         AccountServlet accountServlet = this.accountServlet;
302         if (accountServlet != null) {
303             accountServlet.dispose();
304         }
305         this.accountServlet = null;
306         cleanup();
307         super.dispose();
308     }
309
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;
316         }
317         ScheduledFuture<?> refreshJob = this.checkDataJob;
318         if (refreshJob != null) {
319             refreshJob.cancel(true);
320             this.checkDataJob = null;
321         }
322         ScheduledFuture<?> refreshLogin = this.checkLoginJob;
323         if (refreshLogin != null) {
324             refreshLogin.cancel(true);
325             this.checkLoginJob = null;
326         }
327         ScheduledFuture<?> foceCheckDataJob = this.forceCheckDataJob;
328         if (foceCheckDataJob != null) {
329             foceCheckDataJob.cancel(true);
330             this.forceCheckDataJob = null;
331         }
332         ScheduledFuture<?> refreshAfterCommandJob = this.refreshAfterCommandJob;
333         if (refreshAfterCommandJob != null) {
334             refreshAfterCommandJob.cancel(true);
335             this.refreshAfterCommandJob = null;
336         }
337         ScheduledFuture<?> refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob;
338         if (refreshSmartHomeAfterCommandJob != null) {
339             refreshSmartHomeAfterCommandJob.cancel(true);
340             this.refreshSmartHomeAfterCommandJob = null;
341         }
342         Connection connection = this.connection;
343         if (connection != null) {
344             connection.logout();
345             this.connection = null;
346         }
347         closeWebSocketConnection();
348     }
349
350     private void checkLogin() {
351         try {
352             ThingUID uid = getThing().getUID();
353             logger.debug("check login {}", uid.getAsString());
354
355             synchronized (synchronizeConnection) {
356                 Connection currentConnection = this.connection;
357                 if (currentConnection == null) {
358                     return;
359                 }
360
361                 try {
362                     if (currentConnection.getIsLoggedIn()) {
363                         if (currentConnection.checkRenewSession()) {
364                             setConnection(currentConnection);
365                         }
366                     } else {
367                         // read session data from property
368                         String sessionStore = this.stateStorage.get("sessionStorage");
369
370                         // try use the session data
371                         if (currentConnection.tryRestoreLogin(sessionStore, null)) {
372                             setConnection(currentConnection);
373                         }
374                     }
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"));
379                     }
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());
391                 }
392             }
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);
395         }
396     }
397
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);
404         } else {
405             this.stateStorage.put("sessionStorage", null);
406             updateStatus(ThingStatus.OFFLINE);
407         }
408         closeWebSocketConnection();
409         if (connection != null) {
410             updateDeviceList();
411             updateSmartHomeDeviceList(false);
412             updateFlashBriefingHandlers();
413             updateStatus(ThingStatus.ONLINE);
414             scheduleUpdate();
415             checkData();
416         }
417     }
418
419     void closeWebSocketConnection() {
420         WebSocketConnection webSocketConnection = this.webSocketConnection;
421         this.webSocketConnection = null;
422         if (webSocketConnection != null) {
423             webSocketConnection.close();
424         }
425     }
426
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()) {
432                 try {
433                     this.webSocketConnection = new WebSocketConnection(connection.getAmazonSite(),
434                             connection.getSessionCookies(), this);
435                 } catch (IOException e) {
436                     logger.warn("Web socket connection starting failed", e);
437                 }
438             }
439             return false;
440         }
441         return true;
442     }
443
444     private void checkData() {
445         synchronized (synchronizeConnection) {
446             try {
447                 Connection connection = this.connection;
448                 if (connection != null && connection.getIsLoggedIn()) {
449                     checkDataCounter++;
450                     if (checkDataCounter > 60 || forceCheckDataJob != null) {
451                         checkDataCounter = 0;
452                         forceCheckDataJob = null;
453                     }
454                     if (!checkWebSocketConnection() || checkDataCounter == 0) {
455                         refreshData();
456                     }
457                 }
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);
463             }
464         }
465     }
466
467     private void refreshNotifications(@Nullable JsonCommandPayloadPushNotificationChange pushPayload) {
468         Connection currentConnection = this.connection;
469         if (currentConnection == null) {
470             return;
471         }
472         if (!currentConnection.getIsLoggedIn()) {
473             return;
474         }
475
476         ZonedDateTime timeStamp = ZonedDateTime.now();
477         try {
478             List<JsonNotificationResponse> notifications = currentConnection.notifications();
479             ZonedDateTime timeStampNow = ZonedDateTime.now();
480             echoHandlers.forEach(echoHandler -> echoHandler.updateNotifications(timeStamp, timeStampNow, pushPayload,
481                     notifications));
482         } catch (IOException | URISyntaxException | InterruptedException e) {
483             logger.debug("refreshNotifications failed", e);
484             return;
485         }
486     }
487
488     private void refreshData() {
489         synchronized (synchronizeConnection) {
490             try {
491                 logger.debug("refreshing data {}", getThing().getUID().getAsString());
492
493                 // check if logged in
494                 Connection currentConnection = null;
495                 currentConnection = connection;
496                 if (currentConnection != null) {
497                     if (!currentConnection.getIsLoggedIn()) {
498                         return;
499                     }
500                 }
501                 if (currentConnection == null) {
502                     return;
503                 }
504
505                 // get all devices registered in the account
506                 updateDeviceList();
507                 updateSmartHomeDeviceList(false);
508                 updateFlashBriefingHandlers();
509
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();
517
518                     // update ascending alarm
519                     ascendingAlarmModels = currentConnection.getAscendingAlarm();
520
521                     // update bluetooth states
522                     states = currentConnection.getBluetoothConnectionStates();
523
524                     // update music providers
525                     if (currentConnection.getIsLoggedIn()) {
526                         try {
527                             musicProviders = currentConnection.getMusicProviders();
528                         } catch (HttpException | JsonSyntaxException | ConnectionException e) {
529                             logger.debug("Update music provider failed", e);
530                         }
531                     }
532                 }
533                 // forward device information to echo handler
534                 for (EchoHandler child : echoHandlers) {
535                     Device device = findDeviceJson(child.findSerialNumber());
536
537                     List<JsonNotificationSound> notificationSounds = List.of();
538                     JsonPlaylists playlists = null;
539                     if (device != null && currentConnection.getIsLoggedIn()) {
540                         // update notification sounds
541                         try {
542                             notificationSounds = currentConnection.getNotificationSounds(device);
543                         } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
544                             logger.debug("Update notification sounds failed", e);
545                         }
546                         // update playlists
547                         try {
548                             playlists = currentConnection.getPlaylists(device);
549                         } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
550                             logger.debug("Update playlist failed", e);
551                         }
552                     }
553
554                     BluetoothState state = null;
555                     if (states != null) {
556                         state = states.findStateByDevice(device);
557                     }
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()
565                                     .orElse(null);
566                             deviceNotificationState = deviceNotificationStates.stream()
567                                     .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst()
568                                     .orElse(null);
569                         }
570                     }
571                     child.updateState(this, device, state, deviceNotificationState, ascendingAlarmModel, playlists,
572                             notificationSounds, musicProviders);
573                 }
574
575                 // refresh notifications
576                 refreshNotifications(null);
577
578                 // update account state
579                 updateStatus(ThingStatus.ONLINE);
580
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);
586             }
587         }
588     }
589
590     public @Nullable Device findDeviceJson(@Nullable String serialNumber) {
591         if (serialNumber == null || serialNumber.isEmpty()) {
592             return null;
593         }
594         return this.jsonSerialNumberDeviceMapping.get(serialNumber);
595     }
596
597     public @Nullable Device findDeviceJsonBySerialOrName(@Nullable String serialOrName) {
598         if (serialOrName == null || serialOrName.isEmpty()) {
599             return null;
600         }
601
602         return this.jsonSerialNumberDeviceMapping.values().stream().filter(
603                 d -> serialOrName.equalsIgnoreCase(d.serialNumber) || serialOrName.equalsIgnoreCase(d.accountName))
604                 .findFirst().orElse(null);
605     }
606
607     public List<Device> updateDeviceList() {
608         Connection currentConnection = connection;
609         if (currentConnection == null) {
610             return new ArrayList<>();
611         }
612
613         List<Device> devices = null;
614         try {
615             if (currentConnection.getIsLoggedIn()) {
616                 devices = currentConnection.getDeviceList();
617             }
618         } catch (IOException | URISyntaxException | InterruptedException e) {
619             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
620         }
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));
625         }
626
627         List<WakeWord> wakeWords = currentConnection.getWakeWords();
628         // update handlers
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);
635         }
636
637         if (devices != null) {
638             return devices;
639         }
640         return List.of();
641     }
642
643     public void setEnabledFlashBriefingsJson(String flashBriefingJson) {
644         Connection currentConnection = connection;
645         JsonFeed[] feeds = gson.fromJson(flashBriefingJson, JsonFeed[].class);
646         if (currentConnection != null && feeds != null) {
647             try {
648                 currentConnection.setEnabledFlashBriefings(Arrays.asList(feeds));
649             } catch (IOException | URISyntaxException | InterruptedException e) {
650                 logger.warn("Set flashbriefing profile failed", e);
651             }
652         }
653         updateFlashBriefingHandlers();
654     }
655
656     public String getNewCurrentFlashbriefingConfiguration() {
657         return updateFlashBriefingHandlers();
658     }
659
660     public String updateFlashBriefingHandlers() {
661         Connection currentConnection = connection;
662         if (currentConnection != null) {
663             return updateFlashBriefingHandlers(currentConnection);
664         }
665         return "";
666     }
667
668     private String updateFlashBriefingHandlers(Connection currentConnection) {
669         if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) {
670             updateFlashBriefingProfiles(currentConnection);
671         }
672         boolean flashBriefingProfileFound = false;
673         for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) {
674             flashBriefingProfileFound |= child.initialize(this, currentFlashBriefingJson);
675         }
676         if (flashBriefingProfileFound) {
677             return "";
678         }
679         return this.currentFlashBriefingJson;
680     }
681
682     public @Nullable Connection findConnection() {
683         return this.connection;
684     }
685
686     public String getEnabledFlashBriefingsJson() {
687         Connection currentConnection = this.connection;
688         if (currentConnection == null) {
689             return "";
690         }
691         updateFlashBriefingProfiles(currentConnection);
692         return this.currentFlashBriefingJson;
693     }
694
695     private void updateFlashBriefingProfiles(Connection currentConnection) {
696         try {
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);
704         }
705     }
706
707     @Override
708     public void webSocketCommandReceived(JsonPushCommand pushCommand) {
709         try {
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);
714         }
715     }
716
717     void handleWebsocketCommand(JsonPushCommand pushCommand) {
718         String command = pushCommand.command;
719         if (command != null) {
720             ScheduledFuture<?> refreshDataDelayed = this.refreshAfterCommandJob;
721             switch (command) {
722                 case "PUSH_ACTIVITY":
723                     handlePushActivity(pushCommand.payload);
724                     break;
725                 case "PUSH_DOPPLER_CONNECTION_CHANGE":
726                 case "PUSH_BLUETOOTH_STATE_CHANGE":
727                     if (refreshDataDelayed != null) {
728                         refreshDataDelayed.cancel(false);
729                     }
730                     this.refreshAfterCommandJob = scheduler.schedule(this::refreshAfterCommand, 700,
731                             TimeUnit.MILLISECONDS);
732                     break;
733                 case "PUSH_NOTIFICATION_CHANGE":
734                     JsonCommandPayloadPushNotificationChange pushPayload = gson.fromJson(pushCommand.payload,
735                             JsonCommandPayloadPushNotificationChange.class);
736                     refreshNotifications(pushPayload);
737                     break;
738                 default:
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);
746                         }
747                     }
748                     break;
749             }
750         }
751     }
752
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);
757         }
758     }
759
760     private void handlePushActivity(@Nullable String payload) {
761         if (payload == null) {
762             return;
763         }
764         JsonCommandPayloadPushActivity pushActivity = Objects
765                 .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushActivity.class));
766
767         Key key = pushActivity.key;
768         if (key == null) {
769             return;
770         }
771
772         Connection connection = this.connection;
773         if (connection == null || !connection.getIsLoggedIn()) {
774             return;
775         }
776
777         String search = key.registeredUserId + "#" + key.entryId;
778         connection.getActivities(10, pushActivity.timestamp).stream().filter(activity -> search.equals(activity.id))
779                 .findFirst()
780                 .ifPresent(currentActivity -> currentActivity.getSourceDeviceIds().stream()
781                         .map(sourceDeviceId -> findEchoHandlerBySerialNumber(sourceDeviceId.serialNumber))
782                         .filter(Objects::nonNull).forEach(echoHandler -> Objects.requireNonNull(echoHandler)
783                                 .handlePushActivity(currentActivity)));
784     }
785
786     void refreshAfterCommand() {
787         refreshData();
788     }
789
790     private @Nullable SmartHomeBaseDevice findSmartDeviceHomeJson(SmartHomeDeviceHandler handler) {
791         String id = handler.getId();
792         if (!id.isEmpty()) {
793             return jsonIdSmartHomeDeviceMapping.get(id);
794         }
795         return null;
796     }
797
798     public int getSmartHomeDevicesDiscoveryMode() {
799         return handlerConfig.discoverSmartHome;
800     }
801
802     public List<SmartHomeBaseDevice> updateSmartHomeDeviceList(boolean forceUpdate) {
803         Connection currentConnection = connection;
804         if (currentConnection == null) {
805             return Collections.emptyList();
806         }
807
808         if (!forceUpdate && smartHomeDeviceHandlers.isEmpty() && getSmartHomeDevicesDiscoveryMode() == 0) {
809             return Collections.emptyList();
810         }
811
812         List<SmartHomeBaseDevice> smartHomeDevices = null;
813         try {
814             if (currentConnection.getIsLoggedIn()) {
815                 smartHomeDevices = currentConnection.getSmarthomeDeviceList();
816             }
817         } catch (IOException | URISyntaxException | InterruptedException e) {
818             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
819         }
820         if (smartHomeDevices != null) {
821             // create new id map
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();
827                     if (id != null) {
828                         newJsonIdSmartHomeDeviceMapping.put(id, smartHomeBaseDevice);
829                     }
830                 }
831             }
832             jsonIdSmartHomeDeviceMapping = newJsonIdSmartHomeDeviceMapping;
833         }
834         // update handlers
835         smartHomeDeviceHandlers
836                 .forEach(child -> child.setDeviceAndUpdateThingState(this, findSmartDeviceHomeJson(child)));
837
838         if (smartHomeDevices != null) {
839             return smartHomeDevices;
840         }
841
842         return Collections.emptyList();
843     }
844
845     public void forceDelayedSmartHomeStateUpdate(@Nullable String deviceId) {
846         if (deviceId == null) {
847             return;
848         }
849         synchronized (synchronizeSmartHomeJobScheduler) {
850             requestedDeviceUpdates.add(deviceId);
851             ScheduledFuture<?> refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob;
852             if (refreshSmartHomeAfterCommandJob != null) {
853                 refreshSmartHomeAfterCommandJob.cancel(false);
854             }
855             this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 500,
856                     TimeUnit.MILLISECONDS);
857         }
858     }
859
860     private void updateSmartHomeStateJob() {
861         Set<String> deviceUpdates = new HashSet<>();
862
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);
868                 return;
869             }
870             requestedDeviceUpdates.drainTo(deviceUpdates);
871             this.refreshSmartHomeAfterCommandJob = null;
872         }
873
874         deviceUpdates.forEach(this::updateSmartHomeState);
875     }
876
877     private synchronized void updateSmartHomeState(@Nullable String deviceFilterId) {
878         try {
879             logger.debug("updateSmartHomeState started with deviceFilterId={}", deviceFilterId);
880             Connection connection = this.connection;
881             if (connection == null || !connection.getIsLoggedIn()) {
882                 return;
883             }
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);
889             } else {
890                 SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator = this.smartHomeDeviceStateGroupUpdateCalculator;
891                 if (smartHomeDeviceStateGroupUpdateCalculator == null) {
892                     return;
893                 }
894                 if (smartHomeDeviceHandlers.isEmpty()) {
895                     return;
896                 }
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);
903                 }
904                 smartHomeDeviceStateGroupUpdateCalculator.removeDevicesWithNoUpdate(devicesToUpdate);
905                 devicesToUpdate.stream().filter(Objects::nonNull).forEach(targetDevices::add);
906                 if (targetDevices.isEmpty()) {
907                     return;
908                 }
909             }
910             Map<String, JsonArray> applianceIdToCapabilityStates = connection
911                     .getSmartHomeDeviceStatesJson(targetDevices);
912
913             for (SmartHomeDeviceHandler smartHomeDeviceHandler : smartHomeDeviceHandlers) {
914                 String id = smartHomeDeviceHandler.getId();
915                 if (requestedDeviceUpdates.contains(id)) {
916                     logger.debug("Device update {} suspended", id);
917                     continue;
918                 }
919                 if (deviceFilterId == null || id.equals(deviceFilterId)) {
920                     smartHomeDeviceHandler.updateChannelStates(allDevices, applianceIdToCapabilityStates);
921                 } else {
922                     logger.trace("Id {} not matching filter {}", id, deviceFilterId);
923                 }
924             }
925
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);
931         }
932     }
933 }