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