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