]> git.basschouten.com Git - openhab-addons.git/blob
f3c2497b90a761506b6438c5f26ef0412b5b1812
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.Optional;
29 import java.util.Set;
30 import java.util.concurrent.CopyOnWriteArraySet;
31 import java.util.concurrent.LinkedBlockingQueue;
32 import java.util.concurrent.ScheduledFuture;
33 import java.util.concurrent.TimeUnit;
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.JsonActivities.Activity.SourceDeviceId;
48 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm.AscendingAlarmModel;
49 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates;
50 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState;
51 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushActivity;
52 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushActivity.Key;
53 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushDevice;
54 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushDevice.DopplerId;
55 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushNotificationChange;
56 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState.DeviceNotificationState;
57 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device;
58 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed;
59 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider;
60 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse;
61 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound;
62 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists;
63 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPushCommand;
64 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice;
65 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonWakeWords.WakeWord;
66 import org.openhab.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice;
67 import org.openhab.binding.amazonechocontrol.internal.smarthome.SmartHomeDeviceStateGroupUpdateCalculator;
68 import org.openhab.core.storage.Storage;
69 import org.openhab.core.thing.Bridge;
70 import org.openhab.core.thing.ChannelUID;
71 import org.openhab.core.thing.Thing;
72 import org.openhab.core.thing.ThingStatus;
73 import org.openhab.core.thing.ThingStatusDetail;
74 import org.openhab.core.thing.ThingUID;
75 import org.openhab.core.thing.binding.BaseBridgeHandler;
76 import org.openhab.core.thing.binding.ThingHandler;
77 import org.openhab.core.types.Command;
78 import org.openhab.core.types.RefreshType;
79 import org.openhab.core.types.State;
80 import org.osgi.service.http.HttpService;
81 import org.slf4j.Logger;
82 import org.slf4j.LoggerFactory;
83
84 import com.google.gson.Gson;
85 import com.google.gson.JsonArray;
86 import com.google.gson.JsonSyntaxException;
87
88 /**
89  * Handles the connection to the amazon server.
90  *
91  * @author Michael Geramb - Initial Contribution
92  */
93 @NonNullByDefault
94 public class AccountHandler extends BaseBridgeHandler implements IWebSocketCommandHandler, IAmazonThingHandler {
95     private final Logger logger = LoggerFactory.getLogger(AccountHandler.class);
96     private Storage<String> stateStorage;
97     private @Nullable Connection connection;
98     private @Nullable WebSocketConnection webSocketConnection;
99
100     private final Set<EchoHandler> echoHandlers = new CopyOnWriteArraySet<>();
101     private final Set<SmartHomeDeviceHandler> smartHomeDeviceHandlers = new CopyOnWriteArraySet<>();
102     private final Set<FlashBriefingProfileHandler> flashBriefingProfileHandlers = new CopyOnWriteArraySet<>();
103
104     private final Object synchronizeConnection = new Object();
105     private Map<String, Device> jsonSerialNumberDeviceMapping = new HashMap<>();
106     private Map<String, SmartHomeBaseDevice> jsonIdSmartHomeDeviceMapping = new HashMap<>();
107     private Map<String, SmartHomeDevice> jsonSerialNumberSmartHomeDeviceMapping = new HashMap<>();
108
109     private @Nullable ScheduledFuture<?> checkDataJob;
110     private @Nullable ScheduledFuture<?> checkLoginJob;
111     private @Nullable ScheduledFuture<?> updateSmartHomeStateJob;
112     private @Nullable ScheduledFuture<?> refreshAfterCommandJob;
113     private @Nullable ScheduledFuture<?> refreshSmartHomeAfterCommandJob;
114     private final Object synchronizeSmartHomeJobScheduler = new Object();
115     private @Nullable ScheduledFuture<?> forceCheckDataJob;
116     private String currentFlashBriefingJson = "";
117     private final HttpService httpService;
118     private @Nullable AccountServlet accountServlet;
119     private final Gson gson;
120     private int checkDataCounter;
121     private final LinkedBlockingQueue<String> requestedDeviceUpdates = new LinkedBlockingQueue<>();
122     private @Nullable SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator;
123     private List<ChannelHandler> channelHandlers = new ArrayList<>();
124
125     private AccountHandlerConfig handlerConfig = new AccountHandlerConfig();
126
127     public AccountHandler(Bridge bridge, HttpService httpService, Storage<String> stateStorage, Gson gson) {
128         super(bridge);
129         this.gson = gson;
130         this.httpService = httpService;
131         this.stateStorage = stateStorage;
132         channelHandlers.add(new ChannelHandlerSendMessage(this, this.gson));
133     }
134
135     @Override
136     public void initialize() {
137         handlerConfig = getConfig().as(AccountHandlerConfig.class);
138
139         synchronized (synchronizeConnection) {
140             Connection connection = this.connection;
141             if (connection == null) {
142                 this.connection = new Connection(null, gson);
143             }
144         }
145
146         if (accountServlet == null) {
147             try {
148                 accountServlet = new AccountServlet(httpService, this.getThing().getUID().getId(), this, gson);
149             } catch (IllegalStateException e) {
150                 logger.warn("Failed to create account servlet", e);
151             }
152         }
153
154         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login");
155
156         checkLoginJob = scheduler.scheduleWithFixedDelay(this::checkLogin, 0, 60, TimeUnit.SECONDS);
157         checkDataJob = scheduler.scheduleWithFixedDelay(this::checkData, 4, 60, TimeUnit.SECONDS);
158
159         int pollingIntervalAlexa = handlerConfig.pollingIntervalSmartHomeAlexa;
160         if (pollingIntervalAlexa < 10) {
161             pollingIntervalAlexa = 10;
162         }
163         int pollingIntervalSkills = handlerConfig.pollingIntervalSmartSkills;
164         if (pollingIntervalSkills < 60) {
165             pollingIntervalSkills = 60;
166         }
167         smartHomeDeviceStateGroupUpdateCalculator = new SmartHomeDeviceStateGroupUpdateCalculator(pollingIntervalAlexa,
168                 pollingIntervalSkills);
169         updateSmartHomeStateJob = scheduler.scheduleWithFixedDelay(() -> updateSmartHomeState(null), 20, 10,
170                 TimeUnit.SECONDS);
171     }
172
173     @Override
174     public void updateChannelState(String channelId, State state) {
175         updateState(channelId, state);
176     }
177
178     @Override
179     public void handleCommand(ChannelUID channelUID, Command command) {
180         try {
181             logger.trace("Command '{}' received for channel '{}'", command, channelUID);
182             Connection connection = this.connection;
183             if (connection == null) {
184                 return;
185             }
186
187             String channelId = channelUID.getId();
188             for (ChannelHandler channelHandler : channelHandlers) {
189                 if (channelHandler.tryHandleCommand(new Device(), connection, channelId, command)) {
190                     return;
191                 }
192             }
193             if (command instanceof RefreshType) {
194                 refreshData();
195             }
196         } catch (IOException | URISyntaxException e) {
197             logger.info("handleCommand fails", e);
198         }
199     }
200
201     @Override
202     public void startAnnouncment(Device device, String speak, String bodyText, @Nullable String title,
203             @Nullable Integer volume) throws IOException, URISyntaxException {
204         EchoHandler echoHandler = findEchoHandlerBySerialNumber(device.serialNumber);
205         if (echoHandler != null) {
206             echoHandler.startAnnouncment(device, speak, bodyText, title, volume);
207         }
208     }
209
210     public List<FlashBriefingProfileHandler> getFlashBriefingProfileHandlers() {
211         return new ArrayList<>(flashBriefingProfileHandlers);
212     }
213
214     public List<Device> getLastKnownDevices() {
215         return new ArrayList<>(jsonSerialNumberDeviceMapping.values());
216     }
217
218     public List<SmartHomeBaseDevice> getLastKnownSmartHomeDevices() {
219         return new ArrayList<>(jsonIdSmartHomeDeviceMapping.values());
220     }
221
222     public void addEchoHandler(EchoHandler echoHandler) {
223         if (echoHandlers.add(echoHandler)) {
224
225             forceCheckData();
226         }
227     }
228
229     public void addSmartHomeDeviceHandler(SmartHomeDeviceHandler smartHomeDeviceHandler) {
230         if (smartHomeDeviceHandlers.add(smartHomeDeviceHandler)) {
231             forceCheckData();
232         }
233     }
234
235     public void forceCheckData() {
236         if (forceCheckDataJob == null) {
237             forceCheckDataJob = scheduler.schedule(this::checkData, 1000, TimeUnit.MILLISECONDS);
238         }
239     }
240
241     public @Nullable Thing findThingBySerialNumber(@Nullable String deviceSerialNumber) {
242         EchoHandler echoHandler = findEchoHandlerBySerialNumber(deviceSerialNumber);
243         if (echoHandler != null) {
244             return echoHandler.getThing();
245         }
246         return null;
247     }
248
249     public @Nullable EchoHandler findEchoHandlerBySerialNumber(@Nullable String deviceSerialNumber) {
250         for (EchoHandler echoHandler : echoHandlers) {
251             if (deviceSerialNumber != null && deviceSerialNumber.equals(echoHandler.findSerialNumber())) {
252                 return echoHandler;
253             }
254         }
255         return null;
256     }
257
258     public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBriefingProfileHandler) {
259         flashBriefingProfileHandlers.add(flashBriefingProfileHandler);
260         Connection connection = this.connection;
261         if (connection != null && connection.getIsLoggedIn()) {
262             if (currentFlashBriefingJson.isEmpty()) {
263                 updateFlashBriefingProfiles(connection);
264             }
265             flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson);
266         }
267     }
268
269     private void scheduleUpdate() {
270         checkDataCounter = 999;
271     }
272
273     @Override
274     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
275         super.childHandlerInitialized(childHandler, childThing);
276         scheduleUpdate();
277     }
278
279     @Override
280     public void handleRemoval() {
281         cleanup();
282         super.handleRemoval();
283     }
284
285     @Override
286     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
287         // check for echo handler
288         if (childHandler instanceof EchoHandler) {
289             echoHandlers.remove(childHandler);
290         }
291         // check for flash briefing profile handler
292         if (childHandler instanceof FlashBriefingProfileHandler) {
293             flashBriefingProfileHandlers.remove(childHandler);
294         }
295         // check for flash briefing profile handler
296         if (childHandler instanceof SmartHomeDeviceHandler) {
297             smartHomeDeviceHandlers.remove(childHandler);
298         }
299         super.childHandlerDisposed(childHandler, childThing);
300     }
301
302     @Override
303     public void dispose() {
304         AccountServlet accountServlet = this.accountServlet;
305         if (accountServlet != null) {
306             accountServlet.dispose();
307         }
308         this.accountServlet = null;
309         cleanup();
310         super.dispose();
311     }
312
313     private void cleanup() {
314         logger.debug("cleanup {}", getThing().getUID().getAsString());
315         ScheduledFuture<?> updateSmartHomeStateJob = this.updateSmartHomeStateJob;
316         if (updateSmartHomeStateJob != null) {
317             updateSmartHomeStateJob.cancel(true);
318             this.updateSmartHomeStateJob = null;
319         }
320         ScheduledFuture<?> refreshJob = this.checkDataJob;
321         if (refreshJob != null) {
322             refreshJob.cancel(true);
323             this.checkDataJob = null;
324         }
325         ScheduledFuture<?> refreshLogin = this.checkLoginJob;
326         if (refreshLogin != null) {
327             refreshLogin.cancel(true);
328             this.checkLoginJob = null;
329         }
330         ScheduledFuture<?> foceCheckDataJob = this.forceCheckDataJob;
331         if (foceCheckDataJob != null) {
332             foceCheckDataJob.cancel(true);
333             this.forceCheckDataJob = null;
334         }
335         ScheduledFuture<?> refreshAfterCommandJob = this.refreshAfterCommandJob;
336         if (refreshAfterCommandJob != null) {
337             refreshAfterCommandJob.cancel(true);
338             this.refreshAfterCommandJob = null;
339         }
340         ScheduledFuture<?> refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob;
341         if (refreshSmartHomeAfterCommandJob != null) {
342             refreshSmartHomeAfterCommandJob.cancel(true);
343             this.refreshSmartHomeAfterCommandJob = null;
344         }
345         Connection connection = this.connection;
346         if (connection != null) {
347             connection.logout();
348             this.connection = null;
349         }
350         closeWebSocketConnection();
351     }
352
353     private void checkLogin() {
354         try {
355             ThingUID uid = getThing().getUID();
356             logger.debug("check login {}", uid.getAsString());
357
358             synchronized (synchronizeConnection) {
359                 Connection currentConnection = this.connection;
360                 if (currentConnection == null) {
361                     return;
362                 }
363
364                 try {
365                     if (currentConnection.getIsLoggedIn()) {
366                         if (currentConnection.checkRenewSession()) {
367                             setConnection(currentConnection);
368                         }
369                     } else {
370                         // read session data from property
371                         String sessionStore = this.stateStorage.get("sessionStorage");
372
373                         // try use the session data
374                         if (currentConnection.tryRestoreLogin(sessionStore, null)) {
375                             setConnection(currentConnection);
376                         }
377                     }
378                     if (!currentConnection.getIsLoggedIn()) {
379                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
380                                 "Please login in through web site: http(s)://<YOUROPENHAB>:<YOURPORT>/amazonechocontrol/"
381                                         + URLEncoder.encode(uid.getId(), "UTF8"));
382                     }
383                 } catch (ConnectionException e) {
384                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
385                 } catch (HttpException e) {
386                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
387                 } catch (UnknownHostException e) {
388                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
389                             "Unknown host name '" + e.getMessage() + "'. Maybe your internet connection is offline");
390                 } catch (IOException e) {
391                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
392                 } catch (URISyntaxException e) {
393                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
394                 }
395             }
396         } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
397             logger.error("check login fails with unexpected error", e);
398         }
399     }
400
401     // used to set a valid connection from the web proxy login
402     public void setConnection(@Nullable Connection connection) {
403         this.connection = connection;
404         if (connection != null) {
405             String serializedStorage = connection.serializeLoginData();
406             this.stateStorage.put("sessionStorage", serializedStorage);
407         } else {
408             this.stateStorage.put("sessionStorage", null);
409             updateStatus(ThingStatus.OFFLINE);
410         }
411         closeWebSocketConnection();
412         if (connection != null) {
413             updateDeviceList();
414             updateSmartHomeDeviceList(false);
415             updateFlashBriefingHandlers();
416             updateStatus(ThingStatus.ONLINE);
417             scheduleUpdate();
418             checkData();
419         }
420     }
421
422     void closeWebSocketConnection() {
423         WebSocketConnection webSocketConnection = this.webSocketConnection;
424         this.webSocketConnection = null;
425         if (webSocketConnection != null) {
426             webSocketConnection.close();
427         }
428     }
429
430     private boolean checkWebSocketConnection() {
431         WebSocketConnection webSocketConnection = this.webSocketConnection;
432         if (webSocketConnection == null || webSocketConnection.isClosed()) {
433             Connection connection = this.connection;
434             if (connection != null && connection.getIsLoggedIn()) {
435                 try {
436                     this.webSocketConnection = new WebSocketConnection(connection.getAmazonSite(),
437                             connection.getSessionCookies(), this);
438                 } catch (IOException e) {
439                     logger.warn("Web socket connection starting failed", e);
440                 }
441             }
442             return false;
443         }
444         return true;
445     }
446
447     private void checkData() {
448         synchronized (synchronizeConnection) {
449             try {
450                 Connection connection = this.connection;
451                 if (connection != null && connection.getIsLoggedIn()) {
452                     checkDataCounter++;
453                     if (checkDataCounter > 60 || forceCheckDataJob != null) {
454                         checkDataCounter = 0;
455                         forceCheckDataJob = null;
456                     }
457                     if (!checkWebSocketConnection() || checkDataCounter == 0) {
458                         refreshData();
459                     }
460                 }
461                 logger.debug("checkData {} finished", getThing().getUID().getAsString());
462             } catch (HttpException | JsonSyntaxException | ConnectionException e) {
463                 logger.debug("checkData fails", e);
464             } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
465                 logger.error("checkData fails with unexpected error", e);
466             }
467         }
468     }
469
470     private void refreshNotifications(@Nullable JsonCommandPayloadPushNotificationChange pushPayload) {
471         Connection currentConnection = this.connection;
472         if (currentConnection == null) {
473             return;
474         }
475         if (!currentConnection.getIsLoggedIn()) {
476             return;
477         }
478         JsonNotificationResponse[] notifications;
479         ZonedDateTime timeStamp = ZonedDateTime.now();
480         try {
481             notifications = currentConnection.notifications();
482         } catch (IOException | URISyntaxException e) {
483             logger.debug("refreshNotifications failed", e);
484             return;
485         }
486         ZonedDateTime timeStampNow = ZonedDateTime.now();
487
488         echoHandlers.forEach(
489                 echoHandler -> echoHandler.updateNotifications(timeStamp, timeStampNow, pushPayload, notifications));
490     }
491
492     private void refreshData() {
493         synchronized (synchronizeConnection) {
494             try {
495                 logger.debug("refreshing data {}", getThing().getUID().getAsString());
496
497                 // check if logged in
498                 Connection currentConnection = null;
499                 currentConnection = connection;
500                 if (currentConnection != null) {
501                     if (!currentConnection.getIsLoggedIn()) {
502                         return;
503                     }
504                 }
505                 if (currentConnection == null) {
506                     return;
507                 }
508
509                 // get all devices registered in the account
510                 updateDeviceList();
511                 updateSmartHomeDeviceList(false);
512                 updateFlashBriefingHandlers();
513
514                 DeviceNotificationState[] deviceNotificationStates = null;
515                 AscendingAlarmModel[] ascendingAlarmModels = null;
516                 JsonBluetoothStates states = null;
517                 List<JsonMusicProvider> musicProviders = null;
518                 if (currentConnection.getIsLoggedIn()) {
519                     // update notification states
520                     deviceNotificationStates = currentConnection.getDeviceNotificationStates();
521
522                     // update ascending alarm
523                     ascendingAlarmModels = currentConnection.getAscendingAlarm();
524
525                     // update bluetooth states
526                     states = currentConnection.getBluetoothConnectionStates();
527
528                     // update music providers
529                     if (currentConnection.getIsLoggedIn()) {
530                         try {
531                             musicProviders = currentConnection.getMusicProviders();
532                         } catch (HttpException | JsonSyntaxException | ConnectionException e) {
533                             logger.debug("Update music provider failed", e);
534                         }
535                     }
536                 }
537                 // forward device information to echo handler
538                 for (EchoHandler child : echoHandlers) {
539                     Device device = findDeviceJson(child.findSerialNumber());
540
541                     JsonNotificationSound[] notificationSounds = null;
542                     JsonPlaylists playlists = null;
543                     if (device != null && currentConnection.getIsLoggedIn()) {
544                         // update notification sounds
545                         try {
546                             notificationSounds = currentConnection.getNotificationSounds(device);
547                         } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
548                             logger.debug("Update notification sounds failed", e);
549                         }
550                         // update playlists
551                         try {
552                             playlists = currentConnection.getPlaylists(device);
553                         } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
554                             logger.debug("Update playlist failed", e);
555                         }
556                     }
557
558                     BluetoothState state = null;
559                     if (states != null) {
560                         state = states.findStateByDevice(device);
561                     }
562                     DeviceNotificationState deviceNotificationState = null;
563                     AscendingAlarmModel ascendingAlarmModel = null;
564                     if (device != null) {
565                         final String serialNumber = device.serialNumber;
566                         if (serialNumber != null) {
567                             if (ascendingAlarmModels != null) {
568                                 ascendingAlarmModel = Arrays.stream(ascendingAlarmModels).filter(Objects::nonNull)
569                                         .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst()
570                                         .orElse(null);
571                             }
572                             if (deviceNotificationStates != null) {
573                                 deviceNotificationState = Arrays.stream(deviceNotificationStates)
574                                         .filter(Objects::nonNull)
575                                         .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst()
576                                         .orElse(null);
577                             }
578                         }
579                     }
580                     child.updateState(this, device, state, deviceNotificationState, ascendingAlarmModel, playlists,
581                             notificationSounds, musicProviders);
582                 }
583
584                 // refresh notifications
585                 refreshNotifications(null);
586
587                 // update account state
588                 updateStatus(ThingStatus.ONLINE);
589
590                 logger.debug("refresh data {} finished", getThing().getUID().getAsString());
591             } catch (HttpException | JsonSyntaxException | ConnectionException e) {
592                 logger.debug("refresh data fails", e);
593             } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
594                 logger.error("refresh data fails with unexpected error", e);
595             }
596         }
597     }
598
599     public @Nullable Device findDeviceJson(@Nullable String serialNumber) {
600         Device result = null;
601         if (serialNumber != null && !serialNumber.isEmpty()) {
602             Map<String, Device> jsonSerialNumberDeviceMapping = this.jsonSerialNumberDeviceMapping;
603             result = jsonSerialNumberDeviceMapping.get(serialNumber);
604         }
605         return result;
606     }
607
608     public @Nullable Device findDeviceJsonBySerialOrName(@Nullable String serialOrName) {
609         if (serialOrName == null || serialOrName.isEmpty()) {
610             return null;
611         }
612
613         Optional<Device> device = this.jsonSerialNumberDeviceMapping.values().stream().filter(
614                 d -> serialOrName.equalsIgnoreCase(d.serialNumber) || serialOrName.equalsIgnoreCase(d.accountName))
615                 .findFirst();
616
617         if (device.isPresent()) {
618             return device.get();
619         } else {
620             return null;
621         }
622     }
623
624     public List<Device> updateDeviceList() {
625         Connection currentConnection = connection;
626         if (currentConnection == null) {
627             return new ArrayList<>();
628         }
629
630         List<Device> devices = null;
631         try {
632             if (currentConnection.getIsLoggedIn()) {
633                 devices = currentConnection.getDeviceList();
634             }
635         } catch (IOException | URISyntaxException e) {
636             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
637         }
638         if (devices != null) {
639             // create new device map
640             Map<String, Device> newJsonSerialDeviceMapping = new HashMap<>();
641             for (Device device : devices) {
642                 String serialNumber = device.serialNumber;
643                 if (serialNumber != null) {
644                     newJsonSerialDeviceMapping.put(serialNumber, device);
645                 }
646
647             }
648             jsonSerialNumberDeviceMapping = newJsonSerialDeviceMapping;
649         }
650
651         WakeWord[] wakeWords = currentConnection.getWakeWords();
652         // update handlers
653         for (EchoHandler echoHandler : echoHandlers) {
654             String serialNumber = echoHandler.findSerialNumber();
655             String deviceWakeWord = null;
656             for (WakeWord wakeWord : wakeWords) {
657                 if (wakeWord != null) {
658                     if (serialNumber != null && serialNumber.equals(wakeWord.deviceSerialNumber)) {
659                         deviceWakeWord = wakeWord.wakeWord;
660                         break;
661                     }
662                 }
663             }
664             echoHandler.setDeviceAndUpdateThingState(this, findDeviceJson(serialNumber), deviceWakeWord);
665         }
666
667         if (devices != null) {
668             return devices;
669         }
670         return Collections.emptyList();
671     }
672
673     public void setEnabledFlashBriefingsJson(String flashBriefingJson) {
674         Connection currentConnection = connection;
675         JsonFeed[] feeds = gson.fromJson(flashBriefingJson, JsonFeed[].class);
676         if (currentConnection != null && feeds != null) {
677             try {
678                 currentConnection.setEnabledFlashBriefings(feeds);
679             } catch (IOException | URISyntaxException e) {
680                 logger.warn("Set flashbriefing profile failed", e);
681             }
682         }
683         updateFlashBriefingHandlers();
684     }
685
686     public String getNewCurrentFlashbriefingConfiguration() {
687         return updateFlashBriefingHandlers();
688     }
689
690     public String updateFlashBriefingHandlers() {
691         Connection currentConnection = connection;
692         if (currentConnection != null) {
693             return updateFlashBriefingHandlers(currentConnection);
694         }
695         return "";
696     }
697
698     private String updateFlashBriefingHandlers(Connection currentConnection) {
699         if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) {
700             updateFlashBriefingProfiles(currentConnection);
701         }
702         boolean flashBriefingProfileFound = false;
703         for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) {
704             flashBriefingProfileFound |= child.initialize(this, currentFlashBriefingJson);
705         }
706         if (flashBriefingProfileFound) {
707             return "";
708         }
709         return this.currentFlashBriefingJson;
710     }
711
712     public @Nullable Connection findConnection() {
713         return this.connection;
714     }
715
716     public String getEnabledFlashBriefingsJson() {
717         Connection currentConnection = this.connection;
718         if (currentConnection == null) {
719             return "";
720         }
721         updateFlashBriefingProfiles(currentConnection);
722         return this.currentFlashBriefingJson;
723     }
724
725     private void updateFlashBriefingProfiles(Connection currentConnection) {
726         try {
727             JsonFeed[] feeds = currentConnection.getEnabledFlashBriefings();
728             // Make a copy and remove changeable parts
729             JsonFeed[] forSerializer = new JsonFeed[feeds.length];
730             for (int i = 0; i < feeds.length; i++) {
731                 JsonFeed source = feeds[i];
732                 JsonFeed copy = new JsonFeed();
733                 copy.feedId = source.feedId;
734                 copy.skillId = source.skillId;
735                 // Do not copy imageUrl here, because it will change
736                 forSerializer[i] = copy;
737             }
738             this.currentFlashBriefingJson = gson.toJson(forSerializer);
739         } catch (HttpException | JsonSyntaxException | IOException | URISyntaxException | ConnectionException e) {
740             logger.warn("get flash briefing profiles fails", e);
741         }
742     }
743
744     @Override
745     public void webSocketCommandReceived(JsonPushCommand pushCommand) {
746         try {
747             handleWebsocketCommand(pushCommand);
748         } catch (Exception e) {
749             // should never happen, but if the exception is going out of this function, the binding stop working.
750             logger.warn("handling of websockets fails", e);
751         }
752     }
753
754     void handleWebsocketCommand(JsonPushCommand pushCommand) {
755         String command = pushCommand.command;
756         if (command != null) {
757             ScheduledFuture<?> refreshDataDelayed = this.refreshAfterCommandJob;
758             switch (command) {
759                 case "PUSH_ACTIVITY":
760                     handlePushActivity(pushCommand.payload);
761                     if (refreshDataDelayed != null) {
762                         refreshDataDelayed.cancel(false);
763                     }
764                     this.refreshAfterCommandJob = scheduler.schedule(this::refreshAfterCommand, 700,
765                             TimeUnit.MILLISECONDS);
766                     break;
767                 case "PUSH_DOPPLER_CONNECTION_CHANGE":
768                 case "PUSH_BLUETOOTH_STATE_CHANGE":
769                     if (refreshDataDelayed != null) {
770                         refreshDataDelayed.cancel(false);
771                     }
772                     this.refreshAfterCommandJob = scheduler.schedule(this::refreshAfterCommand, 700,
773                             TimeUnit.MILLISECONDS);
774                     break;
775                 case "PUSH_NOTIFICATION_CHANGE":
776                     JsonCommandPayloadPushNotificationChange pushPayload = gson.fromJson(pushCommand.payload,
777                             JsonCommandPayloadPushNotificationChange.class);
778                     refreshNotifications(pushPayload);
779                     break;
780                 default:
781                     String payload = pushCommand.payload;
782                     if (payload != null && payload.startsWith("{") && payload.endsWith("}")) {
783                         JsonCommandPayloadPushDevice devicePayload = gson.fromJson(payload,
784                                 JsonCommandPayloadPushDevice.class);
785                         DopplerId dopplerId = devicePayload.dopplerId;
786                         if (dopplerId != null) {
787                             handlePushDeviceCommand(dopplerId, command, payload);
788                         }
789                     }
790                     break;
791             }
792         }
793     }
794
795     private void handlePushDeviceCommand(DopplerId dopplerId, String command, String payload) {
796         EchoHandler echoHandler = findEchoHandlerBySerialNumber(dopplerId.deviceSerialNumber);
797         if (echoHandler != null) {
798             echoHandler.handlePushCommand(command, payload);
799         }
800     }
801
802     private void handlePushActivity(@Nullable String payload) {
803         JsonCommandPayloadPushActivity pushActivity = gson.fromJson(payload, JsonCommandPayloadPushActivity.class);
804
805         Key key = pushActivity.key;
806         if (key == null) {
807             return;
808         }
809
810         Connection connection = this.connection;
811         if (connection == null || !connection.getIsLoggedIn()) {
812             return;
813         }
814
815         String search = key.registeredUserId + "#" + key.entryId;
816         Arrays.stream(connection.getActivities(10, pushActivity.timestamp))
817                 .filter(activity -> activity != null && search.equals(activity.id)).findFirst()
818                 .ifPresent(currentActivity -> {
819                     SourceDeviceId[] sourceDeviceIds = currentActivity.sourceDeviceIds;
820                     if (sourceDeviceIds != null) {
821                         Arrays.stream(sourceDeviceIds).filter(Objects::nonNull)
822                                 .map(sourceDeviceId -> findEchoHandlerBySerialNumber(sourceDeviceId.serialNumber))
823                                 .filter(Objects::nonNull)
824                                 .forEach(echoHandler -> echoHandler.handlePushActivity(currentActivity));
825                     }
826                 });
827     }
828
829     void refreshAfterCommand() {
830         refreshData();
831     }
832
833     private @Nullable SmartHomeBaseDevice findSmartDeviceHomeJson(SmartHomeDeviceHandler handler) {
834         String id = handler.getId();
835         if (!id.isEmpty()) {
836             return jsonIdSmartHomeDeviceMapping.get(id);
837         }
838         return null;
839     }
840
841     public int getSmartHomeDevicesDiscoveryMode() {
842         return handlerConfig.discoverSmartHome;
843     }
844
845     public List<SmartHomeBaseDevice> updateSmartHomeDeviceList(boolean forceUpdate) {
846         Connection currentConnection = connection;
847         if (currentConnection == null) {
848             return Collections.emptyList();
849         }
850
851         if (!forceUpdate && smartHomeDeviceHandlers.isEmpty() && getSmartHomeDevicesDiscoveryMode() == 0) {
852             return Collections.emptyList();
853         }
854
855         List<SmartHomeBaseDevice> smartHomeDevices = null;
856         try {
857             if (currentConnection.getIsLoggedIn()) {
858                 smartHomeDevices = currentConnection.getSmarthomeDeviceList();
859             }
860         } catch (IOException | URISyntaxException e) {
861             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
862         }
863         if (smartHomeDevices != null) {
864             // create new id map
865             Map<String, SmartHomeBaseDevice> newJsonIdSmartHomeDeviceMapping = new HashMap<>();
866             for (Object smartHomeDevice : smartHomeDevices) {
867                 if (smartHomeDevice instanceof SmartHomeBaseDevice) {
868                     SmartHomeBaseDevice smartHomeBaseDevice = (SmartHomeBaseDevice) smartHomeDevice;
869                     String id = smartHomeBaseDevice.findId();
870                     if (id != null) {
871                         newJsonIdSmartHomeDeviceMapping.put(id, smartHomeBaseDevice);
872                     }
873                 }
874             }
875             jsonIdSmartHomeDeviceMapping = newJsonIdSmartHomeDeviceMapping;
876         }
877         // update handlers
878         smartHomeDeviceHandlers
879                 .forEach(child -> child.setDeviceAndUpdateThingState(this, findSmartDeviceHomeJson(child)));
880
881         if (smartHomeDevices != null) {
882             Map<String, SmartHomeDevice> newJsonSerialNumberSmartHomeDeviceMapping = new HashMap<>();
883             for (Object smartDevice : smartHomeDevices) {
884                 if (smartDevice instanceof SmartHomeDevice) {
885                     SmartHomeDevice shd = (SmartHomeDevice) smartDevice;
886                     String entityId = shd.entityId;
887                     if (entityId != null) {
888                         newJsonSerialNumberSmartHomeDeviceMapping.put(entityId, shd);
889                     }
890                 }
891             }
892             jsonSerialNumberSmartHomeDeviceMapping = newJsonSerialNumberSmartHomeDeviceMapping;
893         }
894         if (smartHomeDevices != null) {
895             return smartHomeDevices;
896         }
897
898         return Collections.emptyList();
899     }
900
901     public void forceDelayedSmartHomeStateUpdate(@Nullable String deviceId) {
902         if (deviceId == null) {
903             return;
904         }
905         synchronized (synchronizeSmartHomeJobScheduler) {
906             requestedDeviceUpdates.add(deviceId);
907             ScheduledFuture<?> refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob;
908             if (refreshSmartHomeAfterCommandJob != null) {
909                 refreshSmartHomeAfterCommandJob.cancel(false);
910             }
911             this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 500,
912                     TimeUnit.MILLISECONDS);
913         }
914     }
915
916     private void updateSmartHomeStateJob() {
917         Set<String> deviceUpdates = new HashSet<>();
918
919         synchronized (synchronizeSmartHomeJobScheduler) {
920             Connection connection = this.connection;
921             if (connection == null || !connection.getIsLoggedIn()) {
922                 this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 1000,
923                         TimeUnit.MILLISECONDS);
924                 return;
925             }
926             requestedDeviceUpdates.drainTo(deviceUpdates);
927             this.refreshSmartHomeAfterCommandJob = null;
928         }
929
930         deviceUpdates.forEach(this::updateSmartHomeState);
931     }
932
933     private synchronized void updateSmartHomeState(@Nullable String deviceFilterId) {
934         try {
935             logger.debug("updateSmartHomeState started");
936             Connection connection = this.connection;
937             if (connection == null || !connection.getIsLoggedIn()) {
938                 return;
939             }
940             List<SmartHomeBaseDevice> allDevices = getLastKnownSmartHomeDevices();
941             Set<String> applianceIds = new HashSet<>();
942             if (deviceFilterId != null) {
943                 applianceIds.add(deviceFilterId);
944             } else {
945                 SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator = this.smartHomeDeviceStateGroupUpdateCalculator;
946                 if (smartHomeDeviceStateGroupUpdateCalculator == null) {
947                     return;
948                 }
949                 if (smartHomeDeviceHandlers.isEmpty()) {
950                     return;
951                 }
952                 List<SmartHomeDevice> devicesToUpdate = new ArrayList<>();
953                 for (SmartHomeDeviceHandler device : smartHomeDeviceHandlers) {
954                     String id = device.getId();
955                     SmartHomeBaseDevice baseDevice = jsonIdSmartHomeDeviceMapping.get(id);
956                     SmartHomeDeviceHandler.getSupportedSmartHomeDevices(baseDevice, allDevices)
957                             .forEach(devicesToUpdate::add);
958                 }
959                 smartHomeDeviceStateGroupUpdateCalculator.removeDevicesWithNoUpdate(devicesToUpdate);
960                 devicesToUpdate.stream().map(shd -> shd.applianceId).forEach(applianceId -> {
961                     if (applianceId != null) {
962                         applianceIds.add(applianceId);
963                     }
964                 });
965                 if (applianceIds.isEmpty()) {
966                     return;
967                 }
968
969             }
970             Map<String, JsonArray> applianceIdToCapabilityStates = connection
971                     .getSmartHomeDeviceStatesJson(applianceIds);
972
973             for (SmartHomeDeviceHandler smartHomeDeviceHandler : smartHomeDeviceHandlers) {
974                 String id = smartHomeDeviceHandler.getId();
975                 if (requestedDeviceUpdates.contains(id)) {
976                     logger.debug("Device update {} suspended", id);
977                     continue;
978                 }
979                 if (id.equals(deviceFilterId)) {
980                     smartHomeDeviceHandler.updateChannelStates(allDevices, applianceIdToCapabilityStates);
981                 }
982             }
983
984             logger.debug("updateSmartHomeState finished");
985         } catch (HttpException | JsonSyntaxException | ConnectionException e) {
986             logger.debug("updateSmartHomeState fails", e);
987         } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
988             logger.warn("updateSmartHomeState fails with unexpected error", e);
989         }
990     }
991 }