]> git.basschouten.com Git - openhab-addons.git/blob
277b6708376794d8e73f51278d88a22feea18d20
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.amazonechocontrol.internal.handler;
14
15 import java.io.IOException;
16 import java.net.URISyntaxException;
17 import java.net.URLEncoder;
18 import java.net.UnknownHostException;
19 import java.time.ZonedDateTime;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Objects;
28 import java.util.Set;
29 import java.util.concurrent.CopyOnWriteArraySet;
30 import java.util.concurrent.LinkedBlockingQueue;
31 import java.util.concurrent.ScheduledFuture;
32 import java.util.concurrent.TimeUnit;
33 import java.util.stream.Collectors;
34
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.openhab.binding.amazonechocontrol.internal.AccountHandlerConfig;
38 import org.openhab.binding.amazonechocontrol.internal.AccountServlet;
39 import org.openhab.binding.amazonechocontrol.internal.Connection;
40 import org.openhab.binding.amazonechocontrol.internal.ConnectionException;
41 import org.openhab.binding.amazonechocontrol.internal.HttpException;
42 import org.openhab.binding.amazonechocontrol.internal.IWebSocketCommandHandler;
43 import org.openhab.binding.amazonechocontrol.internal.WebSocketConnection;
44 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandler;
45 import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandlerSendMessage;
46 import org.openhab.binding.amazonechocontrol.internal.channelhandler.IAmazonThingHandler;
47 import org.openhab.binding.amazonechocontrol.internal.jsons.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 final 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 startAnnouncement(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.startAnnouncement(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             forceCheckData();
224         }
225     }
226
227     public void addSmartHomeDeviceHandler(SmartHomeDeviceHandler smartHomeDeviceHandler) {
228         if (smartHomeDeviceHandlers.add(smartHomeDeviceHandler)) {
229             forceCheckData();
230         }
231     }
232
233     public void forceCheckData() {
234         if (forceCheckDataJob == null) {
235             forceCheckDataJob = scheduler.schedule(this::checkData, 1000, TimeUnit.MILLISECONDS);
236         }
237     }
238
239     public @Nullable Thing findThingBySerialNumber(@Nullable String deviceSerialNumber) {
240         EchoHandler echoHandler = findEchoHandlerBySerialNumber(deviceSerialNumber);
241         if (echoHandler != null) {
242             return echoHandler.getThing();
243         }
244         return null;
245     }
246
247     public @Nullable EchoHandler findEchoHandlerBySerialNumber(@Nullable String deviceSerialNumber) {
248         for (EchoHandler echoHandler : echoHandlers) {
249             if (deviceSerialNumber != null && deviceSerialNumber.equals(echoHandler.findSerialNumber())) {
250                 return echoHandler;
251             }
252         }
253         return null;
254     }
255
256     public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBriefingProfileHandler) {
257         flashBriefingProfileHandlers.add(flashBriefingProfileHandler);
258         Connection connection = this.connection;
259         if (connection != null && connection.getIsLoggedIn()) {
260             if (currentFlashBriefingJson.isEmpty()) {
261                 updateFlashBriefingProfiles(connection);
262             }
263             flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson);
264         }
265     }
266
267     private void scheduleUpdate() {
268         checkDataCounter = 999;
269     }
270
271     @Override
272     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
273         super.childHandlerInitialized(childHandler, childThing);
274         scheduleUpdate();
275     }
276
277     @Override
278     public void handleRemoval() {
279         cleanup();
280         super.handleRemoval();
281     }
282
283     @Override
284     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
285         // check for echo handler
286         if (childHandler instanceof EchoHandler) {
287             echoHandlers.remove(childHandler);
288         }
289         // check for flash briefing profile handler
290         if (childHandler instanceof FlashBriefingProfileHandler) {
291             flashBriefingProfileHandlers.remove(childHandler);
292         }
293         // check for flash briefing profile handler
294         if (childHandler instanceof SmartHomeDeviceHandler) {
295             smartHomeDeviceHandlers.remove(childHandler);
296         }
297         super.childHandlerDisposed(childHandler, childThing);
298     }
299
300     @Override
301     public void dispose() {
302         AccountServlet accountServlet = this.accountServlet;
303         if (accountServlet != null) {
304             accountServlet.dispose();
305         }
306         this.accountServlet = null;
307         cleanup();
308         super.dispose();
309     }
310
311     private void cleanup() {
312         logger.debug("cleanup {}", getThing().getUID().getAsString());
313         ScheduledFuture<?> updateSmartHomeStateJob = this.updateSmartHomeStateJob;
314         if (updateSmartHomeStateJob != null) {
315             updateSmartHomeStateJob.cancel(true);
316             this.updateSmartHomeStateJob = null;
317         }
318         ScheduledFuture<?> refreshJob = this.checkDataJob;
319         if (refreshJob != null) {
320             refreshJob.cancel(true);
321             this.checkDataJob = null;
322         }
323         ScheduledFuture<?> refreshLogin = this.checkLoginJob;
324         if (refreshLogin != null) {
325             refreshLogin.cancel(true);
326             this.checkLoginJob = null;
327         }
328         ScheduledFuture<?> foceCheckDataJob = this.forceCheckDataJob;
329         if (foceCheckDataJob != null) {
330             foceCheckDataJob.cancel(true);
331             this.forceCheckDataJob = null;
332         }
333         ScheduledFuture<?> refreshAfterCommandJob = this.refreshAfterCommandJob;
334         if (refreshAfterCommandJob != null) {
335             refreshAfterCommandJob.cancel(true);
336             this.refreshAfterCommandJob = null;
337         }
338         ScheduledFuture<?> refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob;
339         if (refreshSmartHomeAfterCommandJob != null) {
340             refreshSmartHomeAfterCommandJob.cancel(true);
341             this.refreshSmartHomeAfterCommandJob = null;
342         }
343         Connection connection = this.connection;
344         if (connection != null) {
345             connection.logout();
346             this.connection = null;
347         }
348         closeWebSocketConnection();
349     }
350
351     private void checkLogin() {
352         try {
353             ThingUID uid = getThing().getUID();
354             logger.debug("check login {}", uid.getAsString());
355
356             synchronized (synchronizeConnection) {
357                 Connection currentConnection = this.connection;
358                 if (currentConnection == null) {
359                     return;
360                 }
361
362                 try {
363                     if (currentConnection.getIsLoggedIn()) {
364                         if (currentConnection.checkRenewSession()) {
365                             setConnection(currentConnection);
366                         }
367                     } else {
368                         // read session data from property
369                         String sessionStore = this.stateStorage.get("sessionStorage");
370
371                         // try use the session data
372                         if (currentConnection.tryRestoreLogin(sessionStore, null)) {
373                             setConnection(currentConnection);
374                         }
375                     }
376                     if (!currentConnection.getIsLoggedIn()) {
377                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
378                                 "Please login in through web site: http(s)://<YOUROPENHAB>:<YOURPORT>/amazonechocontrol/"
379                                         + URLEncoder.encode(uid.getId(), "UTF8"));
380                     }
381                 } catch (ConnectionException e) {
382                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
383                 } catch (HttpException e) {
384                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
385                 } catch (UnknownHostException e) {
386                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
387                             "Unknown host name '" + e.getMessage() + "'. Maybe your internet connection is offline");
388                 } catch (IOException e) {
389                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
390                 } catch (URISyntaxException e) {
391                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
392                 }
393             }
394         } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
395             logger.error("check login fails with unexpected error", e);
396         }
397     }
398
399     // used to set a valid connection from the web proxy login
400     public void setConnection(@Nullable Connection connection) {
401         this.connection = connection;
402         if (connection != null) {
403             String serializedStorage = connection.serializeLoginData();
404             this.stateStorage.put("sessionStorage", serializedStorage);
405         } else {
406             this.stateStorage.put("sessionStorage", null);
407             updateStatus(ThingStatus.OFFLINE);
408         }
409         closeWebSocketConnection();
410         if (connection != null) {
411             updateDeviceList();
412             updateSmartHomeDeviceList(false);
413             updateFlashBriefingHandlers();
414             updateStatus(ThingStatus.ONLINE);
415             scheduleUpdate();
416             checkData();
417         }
418     }
419
420     void closeWebSocketConnection() {
421         WebSocketConnection webSocketConnection = this.webSocketConnection;
422         this.webSocketConnection = null;
423         if (webSocketConnection != null) {
424             webSocketConnection.close();
425         }
426     }
427
428     private boolean checkWebSocketConnection() {
429         WebSocketConnection webSocketConnection = this.webSocketConnection;
430         if (webSocketConnection == null || webSocketConnection.isClosed()) {
431             Connection connection = this.connection;
432             if (connection != null && connection.getIsLoggedIn()) {
433                 try {
434                     this.webSocketConnection = new WebSocketConnection(connection.getAmazonSite(),
435                             connection.getSessionCookies(), this);
436                 } catch (IOException e) {
437                     logger.warn("Web socket connection starting failed", e);
438                 }
439             }
440             return false;
441         }
442         return true;
443     }
444
445     private void checkData() {
446         synchronized (synchronizeConnection) {
447             try {
448                 Connection connection = this.connection;
449                 if (connection != null && connection.getIsLoggedIn()) {
450                     checkDataCounter++;
451                     if (checkDataCounter > 60 || forceCheckDataJob != null) {
452                         checkDataCounter = 0;
453                         forceCheckDataJob = null;
454                     }
455                     if (!checkWebSocketConnection() || checkDataCounter == 0) {
456                         refreshData();
457                     }
458                 }
459                 logger.debug("checkData {} finished", getThing().getUID().getAsString());
460             } catch (HttpException | JsonSyntaxException | ConnectionException e) {
461                 logger.debug("checkData fails", e);
462             } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
463                 logger.error("checkData fails with unexpected error", e);
464             }
465         }
466     }
467
468     private void refreshNotifications(@Nullable JsonCommandPayloadPushNotificationChange pushPayload) {
469         Connection currentConnection = this.connection;
470         if (currentConnection == null) {
471             return;
472         }
473         if (!currentConnection.getIsLoggedIn()) {
474             return;
475         }
476         JsonNotificationResponse[] notifications;
477         ZonedDateTime timeStamp = ZonedDateTime.now();
478         try {
479             notifications = currentConnection.notifications();
480         } catch (IOException | URISyntaxException | InterruptedException e) {
481             logger.debug("refreshNotifications failed", e);
482             return;
483         }
484         ZonedDateTime timeStampNow = ZonedDateTime.now();
485
486         echoHandlers.forEach(
487                 echoHandler -> echoHandler.updateNotifications(timeStamp, timeStampNow, pushPayload, notifications));
488     }
489
490     private void refreshData() {
491         synchronized (synchronizeConnection) {
492             try {
493                 logger.debug("refreshing data {}", getThing().getUID().getAsString());
494
495                 // check if logged in
496                 Connection currentConnection = null;
497                 currentConnection = connection;
498                 if (currentConnection != null) {
499                     if (!currentConnection.getIsLoggedIn()) {
500                         return;
501                     }
502                 }
503                 if (currentConnection == null) {
504                     return;
505                 }
506
507                 // get all devices registered in the account
508                 updateDeviceList();
509                 updateSmartHomeDeviceList(false);
510                 updateFlashBriefingHandlers();
511
512                 DeviceNotificationState[] deviceNotificationStates = null;
513                 AscendingAlarmModel[] ascendingAlarmModels = null;
514                 JsonBluetoothStates states = null;
515                 List<JsonMusicProvider> musicProviders = null;
516                 if (currentConnection.getIsLoggedIn()) {
517                     // update notification states
518                     deviceNotificationStates = currentConnection.getDeviceNotificationStates();
519
520                     // update ascending alarm
521                     ascendingAlarmModels = currentConnection.getAscendingAlarm();
522
523                     // update bluetooth states
524                     states = currentConnection.getBluetoothConnectionStates();
525
526                     // update music providers
527                     if (currentConnection.getIsLoggedIn()) {
528                         try {
529                             musicProviders = currentConnection.getMusicProviders();
530                         } catch (HttpException | JsonSyntaxException | ConnectionException e) {
531                             logger.debug("Update music provider failed", e);
532                         }
533                     }
534                 }
535                 // forward device information to echo handler
536                 for (EchoHandler child : echoHandlers) {
537                     Device device = findDeviceJson(child.findSerialNumber());
538
539                     JsonNotificationSound[] notificationSounds = null;
540                     JsonPlaylists playlists = null;
541                     if (device != null && currentConnection.getIsLoggedIn()) {
542                         // update notification sounds
543                         try {
544                             notificationSounds = currentConnection.getNotificationSounds(device);
545                         } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
546                             logger.debug("Update notification sounds failed", e);
547                         }
548                         // update playlists
549                         try {
550                             playlists = currentConnection.getPlaylists(device);
551                         } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) {
552                             logger.debug("Update playlist failed", e);
553                         }
554                     }
555
556                     BluetoothState state = null;
557                     if (states != null) {
558                         state = states.findStateByDevice(device);
559                     }
560                     DeviceNotificationState deviceNotificationState = null;
561                     AscendingAlarmModel ascendingAlarmModel = null;
562                     if (device != null) {
563                         final String serialNumber = device.serialNumber;
564                         if (serialNumber != null) {
565                             if (ascendingAlarmModels != null) {
566                                 ascendingAlarmModel = Arrays.stream(ascendingAlarmModels).filter(Objects::nonNull)
567                                         .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst()
568                                         .orElse(null);
569                             }
570                             if (deviceNotificationStates != null) {
571                                 deviceNotificationState = Arrays.stream(deviceNotificationStates)
572                                         .filter(Objects::nonNull)
573                                         .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst()
574                                         .orElse(null);
575                             }
576                         }
577                     }
578                     child.updateState(this, device, state, deviceNotificationState, ascendingAlarmModel, playlists,
579                             notificationSounds, musicProviders);
580                 }
581
582                 // refresh notifications
583                 refreshNotifications(null);
584
585                 // update account state
586                 updateStatus(ThingStatus.ONLINE);
587
588                 logger.debug("refresh data {} finished", getThing().getUID().getAsString());
589             } catch (HttpException | JsonSyntaxException | ConnectionException e) {
590                 logger.debug("refresh data fails", e);
591             } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
592                 logger.error("refresh data fails with unexpected error", e);
593             }
594         }
595     }
596
597     public @Nullable Device findDeviceJson(@Nullable String serialNumber) {
598         if (serialNumber == null || serialNumber.isEmpty()) {
599             return null;
600         }
601         return this.jsonSerialNumberDeviceMapping.get(serialNumber);
602     }
603
604     public @Nullable Device findDeviceJsonBySerialOrName(@Nullable String serialOrName) {
605         if (serialOrName == null || serialOrName.isEmpty()) {
606             return null;
607         }
608
609         return this.jsonSerialNumberDeviceMapping.values().stream().filter(
610                 d -> serialOrName.equalsIgnoreCase(d.serialNumber) || serialOrName.equalsIgnoreCase(d.accountName))
611                 .findFirst().orElse(null);
612     }
613
614     public List<Device> updateDeviceList() {
615         Connection currentConnection = connection;
616         if (currentConnection == null) {
617             return new ArrayList<>();
618         }
619
620         List<Device> devices = null;
621         try {
622             if (currentConnection.getIsLoggedIn()) {
623                 devices = currentConnection.getDeviceList();
624             }
625         } catch (IOException | URISyntaxException | InterruptedException e) {
626             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
627         }
628         if (devices != null) {
629             // create new device map
630             jsonSerialNumberDeviceMapping = devices.stream().filter(device -> device.serialNumber != null)
631                     .collect(Collectors.toMap(d -> Objects.requireNonNull(d.serialNumber), d -> d));
632         }
633
634         WakeWord[] wakeWords = currentConnection.getWakeWords();
635         // update handlers
636         for (EchoHandler echoHandler : echoHandlers) {
637             String serialNumber = echoHandler.findSerialNumber();
638             String deviceWakeWord = null;
639             for (WakeWord wakeWord : wakeWords) {
640                 if (wakeWord != null) {
641                     if (serialNumber.equals(wakeWord.deviceSerialNumber)) {
642                         deviceWakeWord = wakeWord.wakeWord;
643                         break;
644                     }
645                 }
646             }
647             echoHandler.setDeviceAndUpdateThingState(this, findDeviceJson(serialNumber), deviceWakeWord);
648         }
649
650         if (devices != null) {
651             return devices;
652         }
653         return List.of();
654     }
655
656     public void setEnabledFlashBriefingsJson(String flashBriefingJson) {
657         Connection currentConnection = connection;
658         JsonFeed[] feeds = gson.fromJson(flashBriefingJson, JsonFeed[].class);
659         if (currentConnection != null && feeds != null) {
660             try {
661                 currentConnection.setEnabledFlashBriefings(feeds);
662             } catch (IOException | URISyntaxException | InterruptedException e) {
663                 logger.warn("Set flashbriefing profile failed", e);
664             }
665         }
666         updateFlashBriefingHandlers();
667     }
668
669     public String getNewCurrentFlashbriefingConfiguration() {
670         return updateFlashBriefingHandlers();
671     }
672
673     public String updateFlashBriefingHandlers() {
674         Connection currentConnection = connection;
675         if (currentConnection != null) {
676             return updateFlashBriefingHandlers(currentConnection);
677         }
678         return "";
679     }
680
681     private String updateFlashBriefingHandlers(Connection currentConnection) {
682         if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) {
683             updateFlashBriefingProfiles(currentConnection);
684         }
685         boolean flashBriefingProfileFound = false;
686         for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) {
687             flashBriefingProfileFound |= child.initialize(this, currentFlashBriefingJson);
688         }
689         if (flashBriefingProfileFound) {
690             return "";
691         }
692         return this.currentFlashBriefingJson;
693     }
694
695     public @Nullable Connection findConnection() {
696         return this.connection;
697     }
698
699     public String getEnabledFlashBriefingsJson() {
700         Connection currentConnection = this.connection;
701         if (currentConnection == null) {
702             return "";
703         }
704         updateFlashBriefingProfiles(currentConnection);
705         return this.currentFlashBriefingJson;
706     }
707
708     private void updateFlashBriefingProfiles(Connection currentConnection) {
709         try {
710             JsonFeed[] feeds = currentConnection.getEnabledFlashBriefings();
711             // Make a copy and remove changeable parts
712             JsonFeed[] forSerializer = new JsonFeed[feeds.length];
713             for (int i = 0; i < feeds.length; i++) {
714                 JsonFeed source = feeds[i];
715                 JsonFeed copy = new JsonFeed();
716                 copy.feedId = source.feedId;
717                 copy.skillId = source.skillId;
718                 // Do not copy imageUrl here, because it will change
719                 forSerializer[i] = copy;
720             }
721             this.currentFlashBriefingJson = gson.toJson(forSerializer);
722         } catch (HttpException | JsonSyntaxException | IOException | URISyntaxException | ConnectionException
723                 | InterruptedException e) {
724             logger.warn("get flash briefing profiles fails", e);
725         }
726     }
727
728     @Override
729     public void webSocketCommandReceived(JsonPushCommand pushCommand) {
730         try {
731             handleWebsocketCommand(pushCommand);
732         } catch (Exception e) {
733             // should never happen, but if the exception is going out of this function, the binding stop working.
734             logger.warn("handling of websockets fails", e);
735         }
736     }
737
738     void handleWebsocketCommand(JsonPushCommand pushCommand) {
739         String command = pushCommand.command;
740         if (command != null) {
741             ScheduledFuture<?> refreshDataDelayed = this.refreshAfterCommandJob;
742             switch (command) {
743                 case "PUSH_ACTIVITY":
744                     handlePushActivity(pushCommand.payload);
745                     break;
746                 case "PUSH_DOPPLER_CONNECTION_CHANGE":
747                 case "PUSH_BLUETOOTH_STATE_CHANGE":
748                     if (refreshDataDelayed != null) {
749                         refreshDataDelayed.cancel(false);
750                     }
751                     this.refreshAfterCommandJob = scheduler.schedule(this::refreshAfterCommand, 700,
752                             TimeUnit.MILLISECONDS);
753                     break;
754                 case "PUSH_NOTIFICATION_CHANGE":
755                     JsonCommandPayloadPushNotificationChange pushPayload = gson.fromJson(pushCommand.payload,
756                             JsonCommandPayloadPushNotificationChange.class);
757                     refreshNotifications(pushPayload);
758                     break;
759                 default:
760                     String payload = pushCommand.payload;
761                     if (payload != null && payload.startsWith("{") && payload.endsWith("}")) {
762                         JsonCommandPayloadPushDevice devicePayload = Objects
763                                 .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushDevice.class));
764                         DopplerId dopplerId = devicePayload.dopplerId;
765                         if (dopplerId != null) {
766                             handlePushDeviceCommand(dopplerId, command, payload);
767                         }
768                     }
769                     break;
770             }
771         }
772     }
773
774     private void handlePushDeviceCommand(DopplerId dopplerId, String command, String payload) {
775         EchoHandler echoHandler = findEchoHandlerBySerialNumber(dopplerId.deviceSerialNumber);
776         if (echoHandler != null) {
777             echoHandler.handlePushCommand(command, payload);
778         }
779     }
780
781     private void handlePushActivity(@Nullable String payload) {
782         if (payload == null) {
783             return;
784         }
785         JsonCommandPayloadPushActivity pushActivity = Objects
786                 .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushActivity.class));
787
788         Key key = pushActivity.key;
789         if (key == null) {
790             return;
791         }
792
793         Connection connection = this.connection;
794         if (connection == null || !connection.getIsLoggedIn()) {
795             return;
796         }
797
798         String search = key.registeredUserId + "#" + key.entryId;
799         Arrays.stream(connection.getActivities(10, pushActivity.timestamp))
800                 .filter(activity -> activity != null && search.equals(activity.id)).findFirst()
801                 .ifPresent(currentActivity -> {
802                     SourceDeviceId[] sourceDeviceIds = currentActivity.sourceDeviceIds;
803                     if (sourceDeviceIds != null) {
804                         Arrays.stream(sourceDeviceIds).filter(Objects::nonNull)
805                                 .map(sourceDeviceId -> findEchoHandlerBySerialNumber(sourceDeviceId.serialNumber))
806                                 .filter(Objects::nonNull).forEach(echoHandler -> Objects.requireNonNull(echoHandler)
807                                         .handlePushActivity(currentActivity));
808                     }
809                 });
810     }
811
812     void refreshAfterCommand() {
813         refreshData();
814     }
815
816     private @Nullable SmartHomeBaseDevice findSmartDeviceHomeJson(SmartHomeDeviceHandler handler) {
817         String id = handler.getId();
818         if (!id.isEmpty()) {
819             return jsonIdSmartHomeDeviceMapping.get(id);
820         }
821         return null;
822     }
823
824     public int getSmartHomeDevicesDiscoveryMode() {
825         return handlerConfig.discoverSmartHome;
826     }
827
828     public List<SmartHomeBaseDevice> updateSmartHomeDeviceList(boolean forceUpdate) {
829         Connection currentConnection = connection;
830         if (currentConnection == null) {
831             return Collections.emptyList();
832         }
833
834         if (!forceUpdate && smartHomeDeviceHandlers.isEmpty() && getSmartHomeDevicesDiscoveryMode() == 0) {
835             return Collections.emptyList();
836         }
837
838         List<SmartHomeBaseDevice> smartHomeDevices = null;
839         try {
840             if (currentConnection.getIsLoggedIn()) {
841                 smartHomeDevices = currentConnection.getSmarthomeDeviceList();
842             }
843         } catch (IOException | URISyntaxException | InterruptedException e) {
844             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
845         }
846         if (smartHomeDevices != null) {
847             // create new id map
848             Map<String, SmartHomeBaseDevice> newJsonIdSmartHomeDeviceMapping = new HashMap<>();
849             for (Object smartHomeDevice : smartHomeDevices) {
850                 if (smartHomeDevice instanceof SmartHomeBaseDevice) {
851                     SmartHomeBaseDevice smartHomeBaseDevice = (SmartHomeBaseDevice) smartHomeDevice;
852                     String id = smartHomeBaseDevice.findId();
853                     if (id != null) {
854                         newJsonIdSmartHomeDeviceMapping.put(id, smartHomeBaseDevice);
855                     }
856                 }
857             }
858             jsonIdSmartHomeDeviceMapping = newJsonIdSmartHomeDeviceMapping;
859         }
860         // update handlers
861         smartHomeDeviceHandlers
862                 .forEach(child -> child.setDeviceAndUpdateThingState(this, findSmartDeviceHomeJson(child)));
863
864         if (smartHomeDevices != null) {
865             return smartHomeDevices;
866         }
867
868         return Collections.emptyList();
869     }
870
871     public void forceDelayedSmartHomeStateUpdate(@Nullable String deviceId) {
872         if (deviceId == null) {
873             return;
874         }
875         synchronized (synchronizeSmartHomeJobScheduler) {
876             requestedDeviceUpdates.add(deviceId);
877             ScheduledFuture<?> refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob;
878             if (refreshSmartHomeAfterCommandJob != null) {
879                 refreshSmartHomeAfterCommandJob.cancel(false);
880             }
881             this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 500,
882                     TimeUnit.MILLISECONDS);
883         }
884     }
885
886     private void updateSmartHomeStateJob() {
887         Set<String> deviceUpdates = new HashSet<>();
888
889         synchronized (synchronizeSmartHomeJobScheduler) {
890             Connection connection = this.connection;
891             if (connection == null || !connection.getIsLoggedIn()) {
892                 this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 1000,
893                         TimeUnit.MILLISECONDS);
894                 return;
895             }
896             requestedDeviceUpdates.drainTo(deviceUpdates);
897             this.refreshSmartHomeAfterCommandJob = null;
898         }
899
900         deviceUpdates.forEach(this::updateSmartHomeState);
901     }
902
903     private synchronized void updateSmartHomeState(@Nullable String deviceFilterId) {
904         try {
905             logger.debug("updateSmartHomeState started with deviceFilterId={}", deviceFilterId);
906             Connection connection = this.connection;
907             if (connection == null || !connection.getIsLoggedIn()) {
908                 return;
909             }
910             List<SmartHomeBaseDevice> allDevices = getLastKnownSmartHomeDevices();
911             Set<SmartHomeBaseDevice> targetDevices = new HashSet<>();
912             if (deviceFilterId != null) {
913                 allDevices.stream().filter(d -> deviceFilterId.equals(d.findId())).findFirst()
914                         .ifPresent(targetDevices::add);
915             } else {
916                 SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator = this.smartHomeDeviceStateGroupUpdateCalculator;
917                 if (smartHomeDeviceStateGroupUpdateCalculator == null) {
918                     return;
919                 }
920                 if (smartHomeDeviceHandlers.isEmpty()) {
921                     return;
922                 }
923                 List<SmartHomeDevice> devicesToUpdate = new ArrayList<>();
924                 for (SmartHomeDeviceHandler device : smartHomeDeviceHandlers) {
925                     String id = device.getId();
926                     SmartHomeBaseDevice baseDevice = jsonIdSmartHomeDeviceMapping.get(id);
927                     SmartHomeDeviceHandler.getSupportedSmartHomeDevices(baseDevice, allDevices)
928                             .forEach(devicesToUpdate::add);
929                 }
930                 smartHomeDeviceStateGroupUpdateCalculator.removeDevicesWithNoUpdate(devicesToUpdate);
931                 devicesToUpdate.stream().filter(Objects::nonNull).forEach(targetDevices::add);
932                 if (targetDevices.isEmpty()) {
933                     return;
934                 }
935             }
936             Map<String, JsonArray> applianceIdToCapabilityStates = connection
937                     .getSmartHomeDeviceStatesJson(targetDevices);
938
939             for (SmartHomeDeviceHandler smartHomeDeviceHandler : smartHomeDeviceHandlers) {
940                 String id = smartHomeDeviceHandler.getId();
941                 if (requestedDeviceUpdates.contains(id)) {
942                     logger.debug("Device update {} suspended", id);
943                     continue;
944                 }
945                 if (deviceFilterId == null || id.equals(deviceFilterId)) {
946                     smartHomeDeviceHandler.updateChannelStates(allDevices, applianceIdToCapabilityStates);
947                 } else {
948                     logger.trace("Id {} not matching filter {}", id, deviceFilterId);
949                 }
950             }
951
952             logger.debug("updateSmartHomeState finished");
953         } catch (HttpException | JsonSyntaxException | ConnectionException e) {
954             logger.debug("updateSmartHomeState fails", e);
955         } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
956             logger.warn("updateSmartHomeState fails with unexpected error", e);
957         }
958     }
959 }