]> git.basschouten.com Git - openhab-addons.git/blob
7cfe8778f04213b5c3da185f69b62af996dd2775
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.hdpowerview.internal.handler;
14
15 import java.time.Duration;
16 import java.time.Instant;
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Map.Entry;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.concurrent.CopyOnWriteArrayList;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
31 import org.openhab.binding.hdpowerview.internal.HDPowerViewTranslationProvider;
32 import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
33 import org.openhab.binding.hdpowerview.internal.builders.AutomationChannelBuilder;
34 import org.openhab.binding.hdpowerview.internal.builders.SceneChannelBuilder;
35 import org.openhab.binding.hdpowerview.internal.builders.SceneGroupChannelBuilder;
36 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewHubConfiguration;
37 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
38 import org.openhab.binding.hdpowerview.internal.dto.Firmware;
39 import org.openhab.binding.hdpowerview.internal.dto.HubFirmware;
40 import org.openhab.binding.hdpowerview.internal.dto.Scene;
41 import org.openhab.binding.hdpowerview.internal.dto.SceneCollection;
42 import org.openhab.binding.hdpowerview.internal.dto.ScheduledEvent;
43 import org.openhab.binding.hdpowerview.internal.dto.ShadeData;
44 import org.openhab.binding.hdpowerview.internal.dto.UserData;
45 import org.openhab.binding.hdpowerview.internal.dto.responses.SceneCollections;
46 import org.openhab.binding.hdpowerview.internal.dto.responses.Scenes;
47 import org.openhab.binding.hdpowerview.internal.dto.responses.ScheduledEvents;
48 import org.openhab.binding.hdpowerview.internal.dto.responses.Shades;
49 import org.openhab.binding.hdpowerview.internal.exceptions.HubException;
50 import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException;
51 import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException;
52 import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
53 import org.openhab.core.library.CoreItemFactory;
54 import org.openhab.core.library.types.OnOffType;
55 import org.openhab.core.thing.Bridge;
56 import org.openhab.core.thing.Channel;
57 import org.openhab.core.thing.ChannelGroupUID;
58 import org.openhab.core.thing.ChannelUID;
59 import org.openhab.core.thing.Thing;
60 import org.openhab.core.thing.ThingStatus;
61 import org.openhab.core.thing.ThingStatusDetail;
62 import org.openhab.core.thing.ThingStatusInfo;
63 import org.openhab.core.thing.ThingUID;
64 import org.openhab.core.thing.binding.BaseBridgeHandler;
65 import org.openhab.core.thing.binding.ThingHandler;
66 import org.openhab.core.thing.binding.builder.ChannelBuilder;
67 import org.openhab.core.thing.type.ChannelTypeUID;
68 import org.openhab.core.types.Command;
69 import org.openhab.core.types.RefreshType;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
72
73 /**
74  * The {@link HDPowerViewHubHandler} is responsible for handling commands, which
75  * are sent to one of the channels.
76  *
77  * @author Andy Lintner - Initial contribution
78  * @author Andrew Fiddian-Green - Added support for secondary rail positions
79  * @author Jacob Laursen - Added support for scene groups and automations
80  */
81 @NonNullByDefault
82 public class HDPowerViewHubHandler extends BaseBridgeHandler {
83
84     private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubHandler.class);
85     private final HttpClient httpClient;
86     private final HDPowerViewTranslationProvider translationProvider;
87     private final ConcurrentHashMap<ThingUID, ShadeData> pendingShadeInitializations = new ConcurrentHashMap<>();
88     private final Duration firmwareVersionValidityPeriod = Duration.ofDays(1);
89
90     private long refreshInterval;
91     private long hardRefreshPositionInterval;
92     private long hardRefreshBatteryLevelInterval;
93
94     private @NonNullByDefault({}) HDPowerViewWebTargets webTargets;
95     private @Nullable ScheduledFuture<?> pollFuture;
96     private @Nullable ScheduledFuture<?> hardRefreshPositionFuture;
97     private @Nullable ScheduledFuture<?> hardRefreshBatteryLevelFuture;
98
99     private List<Scene> sceneCache = new CopyOnWriteArrayList<>();
100     private List<SceneCollection> sceneCollectionCache = new CopyOnWriteArrayList<>();
101     private List<ScheduledEvent> scheduledEventCache = new CopyOnWriteArrayList<>();
102     private Instant userDataUpdated = Instant.MIN;
103     private Boolean deprecatedChannelsCreated = false;
104
105     private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
106             HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE);
107
108     private final ChannelTypeUID sceneGroupChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
109             HDPowerViewBindingConstants.CHANNELTYPE_SCENE_GROUP_ACTIVATE);
110
111     private final ChannelTypeUID automationChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
112             HDPowerViewBindingConstants.CHANNELTYPE_AUTOMATION_ENABLED);
113
114     public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient,
115             HDPowerViewTranslationProvider translationProvider) {
116         super(bridge);
117         this.httpClient = httpClient;
118         this.translationProvider = translationProvider;
119     }
120
121     @Override
122     public void handleCommand(ChannelUID channelUID, Command command) {
123         if (RefreshType.REFRESH == command) {
124             requestRefreshShadePositions();
125             return;
126         }
127
128         Channel channel = getThing().getChannel(channelUID.getId());
129         if (channel == null) {
130             return;
131         }
132
133         try {
134             int id = Integer.parseInt(channelUID.getIdWithoutGroup());
135             if (sceneChannelTypeUID.equals(channel.getChannelTypeUID()) && OnOffType.ON == command) {
136                 webTargets.activateScene(id);
137                 // Reschedule soft poll for immediate shade position update.
138                 scheduleSoftPoll();
139             } else if (sceneGroupChannelTypeUID.equals(channel.getChannelTypeUID()) && OnOffType.ON == command) {
140                 webTargets.activateSceneCollection(id);
141                 // Reschedule soft poll for immediate shade position update.
142                 scheduleSoftPoll();
143             } else if (automationChannelTypeUID.equals(channel.getChannelTypeUID())) {
144                 webTargets.enableScheduledEvent(id, OnOffType.ON == command);
145             }
146         } catch (HubMaintenanceException e) {
147             // exceptions are logged in HDPowerViewWebTargets
148             userDataUpdated = Instant.MIN;
149         } catch (NumberFormatException | HubException e) {
150             logger.debug("Unexpected error {}", e.getMessage());
151         }
152     }
153
154     @Override
155     public void initialize() {
156         logger.debug("Initializing hub");
157         HDPowerViewHubConfiguration config = getConfigAs(HDPowerViewHubConfiguration.class);
158         String host = config.host;
159
160         if (host == null || host.isEmpty()) {
161             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
162                     "@text/offline.conf-error.no-host-address");
163             return;
164         }
165
166         pendingShadeInitializations.clear();
167         webTargets = new HDPowerViewWebTargets(httpClient, host);
168         refreshInterval = config.refresh;
169         hardRefreshPositionInterval = config.hardRefresh;
170         hardRefreshBatteryLevelInterval = config.hardRefreshBatteryLevel;
171         initializeChannels();
172         userDataUpdated = Instant.MIN;
173
174         updateStatus(ThingStatus.UNKNOWN);
175         schedulePoll();
176     }
177
178     private void initializeChannels() {
179         // Rebuild dynamic channels and synchronize with cache.
180         updateThing(editThing().withChannels(new ArrayList<Channel>()).build());
181         sceneCache.clear();
182         sceneCollectionCache.clear();
183         scheduledEventCache.clear();
184         deprecatedChannelsCreated = false;
185     }
186
187     public HDPowerViewWebTargets getWebTargets() {
188         return webTargets;
189     }
190
191     @Override
192     public void handleRemoval() {
193         super.handleRemoval();
194         stopPoll();
195     }
196
197     @Override
198     public void dispose() {
199         super.dispose();
200         stopPoll();
201         pendingShadeInitializations.clear();
202     }
203
204     @Override
205     public void childHandlerInitialized(final ThingHandler childHandler, final Thing childThing) {
206         logger.debug("Child handler initialized: {}", childThing.getUID());
207         if (childHandler instanceof HDPowerViewShadeHandler) {
208             ShadeData shadeData = pendingShadeInitializations.remove(childThing.getUID());
209             if (shadeData != null) {
210                 if (shadeData.id > 0) {
211                     updateShadeThing(shadeData.id, childThing, shadeData);
212                 } else {
213                     updateUnknownShadeThing(childThing);
214                 }
215             }
216         }
217         super.childHandlerInitialized(childHandler, childThing);
218     }
219
220     @Override
221     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
222         logger.debug("Child handler disposed: {}", childThing.getUID());
223         if (childHandler instanceof HDPowerViewShadeHandler) {
224             pendingShadeInitializations.remove(childThing.getUID());
225         }
226         super.childHandlerDisposed(childHandler, childThing);
227     }
228
229     private void schedulePoll() {
230         scheduleSoftPoll();
231         scheduleHardPoll();
232     }
233
234     private void scheduleSoftPoll() {
235         ScheduledFuture<?> future = this.pollFuture;
236         if (future != null) {
237             future.cancel(false);
238         }
239         logger.debug("Scheduling poll every {} ms", refreshInterval);
240         this.pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, refreshInterval, TimeUnit.MILLISECONDS);
241     }
242
243     private void scheduleHardPoll() {
244         ScheduledFuture<?> future = this.hardRefreshPositionFuture;
245         if (future != null) {
246             future.cancel(false);
247         }
248         if (hardRefreshPositionInterval > 0) {
249             logger.debug("Scheduling hard position refresh every {} minutes", hardRefreshPositionInterval);
250             this.hardRefreshPositionFuture = scheduler.scheduleWithFixedDelay(this::requestRefreshShadePositions, 1,
251                     hardRefreshPositionInterval, TimeUnit.MINUTES);
252         }
253
254         future = this.hardRefreshBatteryLevelFuture;
255         if (future != null) {
256             future.cancel(false);
257         }
258         if (hardRefreshBatteryLevelInterval > 0) {
259             logger.debug("Scheduling hard battery level refresh every {} hours", hardRefreshBatteryLevelInterval);
260             this.hardRefreshBatteryLevelFuture = scheduler.scheduleWithFixedDelay(
261                     this::requestRefreshShadeBatteryLevels, 1, hardRefreshBatteryLevelInterval, TimeUnit.HOURS);
262         }
263     }
264
265     private synchronized void stopPoll() {
266         ScheduledFuture<?> future = this.pollFuture;
267         if (future != null) {
268             future.cancel(true);
269         }
270         this.pollFuture = null;
271
272         future = this.hardRefreshPositionFuture;
273         if (future != null) {
274             future.cancel(true);
275         }
276         this.hardRefreshPositionFuture = null;
277
278         future = this.hardRefreshBatteryLevelFuture;
279         if (future != null) {
280             future.cancel(true);
281         }
282         this.hardRefreshBatteryLevelFuture = null;
283     }
284
285     private synchronized void poll() {
286         try {
287             updateUserDataProperties();
288         } catch (HubException e) {
289             logger.warn("Failed to update firmware properties: {}", e.getMessage());
290         }
291
292         try {
293             logger.debug("Polling for state");
294             pollShades();
295
296             List<Scene> scenes = updateSceneChannels();
297             List<SceneCollection> sceneCollections = updateSceneGroupChannels();
298             List<ScheduledEvent> scheduledEvents = updateAutomationChannels(scenes, sceneCollections);
299
300             // Scheduled events should also have their current state updated if event has been
301             // enabled or disabled through app or other integration.
302             updateAutomationStates(scheduledEvents);
303         } catch (HubInvalidResponseException e) {
304             Throwable cause = e.getCause();
305             if (cause == null) {
306                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
307             } else {
308                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
309             }
310         } catch (HubMaintenanceException e) {
311             // exceptions are logged in HDPowerViewWebTargets
312             userDataUpdated = Instant.MIN;
313         } catch (HubException e) {
314             logger.warn("Error connecting to bridge: {}", e.getMessage());
315             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
316             userDataUpdated = Instant.MIN;
317         }
318     }
319
320     private void updateUserDataProperties()
321             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
322         if (userDataUpdated.isAfter(Instant.now().minus(firmwareVersionValidityPeriod))) {
323             return;
324         }
325
326         UserData userData = webTargets.getUserData();
327         Map<String, String> properties = editProperties();
328         HubFirmware firmwareVersions = userData.firmware;
329         if (firmwareVersions != null) {
330             updateFirmwareProperties(properties, firmwareVersions);
331         }
332         String serialNumber = userData.serialNumber;
333         if (serialNumber != null) {
334             properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
335         }
336         String macAddress = userData.macAddress;
337         if (macAddress != null) {
338             properties.put(Thing.PROPERTY_MAC_ADDRESS, macAddress);
339         }
340         String hubName = userData.getHubName();
341         if (!hubName.isEmpty()) {
342             properties.put(HDPowerViewBindingConstants.PROPERTY_HUB_NAME, hubName);
343         }
344         updateProperties(properties);
345         userDataUpdated = Instant.now();
346     }
347
348     private void updateFirmwareProperties(Map<String, String> properties, HubFirmware firmwareVersions) {
349         Firmware mainProcessor = firmwareVersions.mainProcessor;
350         if (mainProcessor == null) {
351             logger.warn("Main processor firmware version missing in response.");
352             return;
353         }
354         logger.debug("Main processor firmware version received: {}, {}", mainProcessor.name, mainProcessor.toString());
355         String mainProcessorName = mainProcessor.name;
356         if (mainProcessorName != null) {
357             properties.put(HDPowerViewBindingConstants.PROPERTY_FIRMWARE_NAME, mainProcessorName);
358         }
359         properties.put(Thing.PROPERTY_FIRMWARE_VERSION, mainProcessor.toString());
360         Firmware radio = firmwareVersions.radio;
361         if (radio != null) {
362             logger.debug("Radio firmware version received: {}", radio.toString());
363             properties.put(HDPowerViewBindingConstants.PROPERTY_RADIO_FIRMWARE_VERSION, radio.toString());
364         }
365     }
366
367     private void pollShades() throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
368         Shades shades = webTargets.getShades();
369         List<ShadeData> shadesData = shades.shadeData;
370         if (shadesData == null) {
371             throw new HubInvalidResponseException("Missing 'shades.shadeData' element");
372         }
373
374         updateStatus(ThingStatus.ONLINE);
375         logger.debug("Received data for {} shades", shadesData.size());
376
377         Map<Integer, ShadeData> idShadeDataMap = getIdShadeDataMap(shadesData);
378         Map<Thing, Integer> thingIdMap = getShadeThingIdMap();
379         for (Entry<Thing, Integer> item : thingIdMap.entrySet()) {
380             Thing thing = item.getKey();
381             int shadeId = item.getValue();
382             ShadeData shadeData = idShadeDataMap.get(shadeId);
383             if (shadeData != null) {
384                 updateShadeThing(shadeId, thing, shadeData);
385             } else {
386                 updateUnknownShadeThing(thing);
387             }
388         }
389     }
390
391     private void updateShadeThing(int shadeId, Thing thing, ShadeData shadeData) {
392         HDPowerViewShadeHandler thingHandler = ((HDPowerViewShadeHandler) thing.getHandler());
393         if (thingHandler == null) {
394             logger.debug("Shade '{}' handler not initialized", shadeId);
395             pendingShadeInitializations.put(thing.getUID(), shadeData);
396             return;
397         }
398         ThingStatus thingStatus = thingHandler.getThing().getStatus();
399         switch (thingStatus) {
400             case UNKNOWN:
401             case ONLINE:
402             case OFFLINE:
403                 logger.debug("Updating shade '{}'", shadeId);
404                 thingHandler.onReceiveUpdate(shadeData);
405                 break;
406             case UNINITIALIZED:
407             case INITIALIZING:
408                 logger.debug("Shade '{}' handler not yet ready; status: {}", shadeId, thingStatus);
409                 pendingShadeInitializations.put(thing.getUID(), shadeData);
410                 break;
411             case REMOVING:
412             case REMOVED:
413             default:
414                 logger.debug("Ignoring shade update for shade '{}' in status {}", shadeId, thingStatus);
415                 break;
416         }
417     }
418
419     private void updateUnknownShadeThing(Thing thing) {
420         String shadeId = thing.getUID().getId();
421         logger.debug("Shade '{}' has no data in hub", shadeId);
422         HDPowerViewShadeHandler thingHandler = ((HDPowerViewShadeHandler) thing.getHandler());
423         if (thingHandler == null) {
424             logger.debug("Shade '{}' handler not initialized", shadeId);
425             pendingShadeInitializations.put(thing.getUID(), new ShadeData());
426             return;
427         }
428         ThingStatus thingStatus = thingHandler.getThing().getStatus();
429         switch (thingStatus) {
430             case UNKNOWN:
431             case ONLINE:
432             case OFFLINE:
433                 thing.setStatusInfo(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.GONE,
434                         "@text/offline.gone.shade-unknown-to-hub"));
435                 break;
436             case UNINITIALIZED:
437             case INITIALIZING:
438                 logger.debug("Shade '{}' handler not yet ready; status: {}", shadeId, thingStatus);
439                 pendingShadeInitializations.put(thing.getUID(), new ShadeData());
440                 break;
441             case REMOVING:
442             case REMOVED:
443             default:
444                 logger.debug("Ignoring shade status update for shade '{}' in status {}", shadeId, thingStatus);
445                 break;
446         }
447     }
448
449     private List<Scene> fetchScenes()
450             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
451         Scenes scenes = webTargets.getScenes();
452         List<Scene> sceneData = scenes.sceneData;
453         if (sceneData == null) {
454             throw new HubInvalidResponseException("Missing 'scenes.sceneData' element");
455         }
456         logger.debug("Received data for {} scenes", sceneData.size());
457
458         return sceneData;
459     }
460
461     private List<Scene> updateSceneChannels()
462             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
463         List<Scene> scenes = fetchScenes();
464
465         if (scenes.size() == sceneCache.size() && sceneCache.containsAll(scenes)) {
466             // Duplicates are not allowed. Reordering is not supported.
467             logger.debug("Preserving scene channels, no changes detected");
468             return scenes;
469         }
470
471         logger.debug("Updating all scene channels, changes detected");
472         sceneCache = new CopyOnWriteArrayList<Scene>(scenes);
473
474         List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
475         allChannels.removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES.equals(c.getUID().getGroupId()));
476
477         SceneChannelBuilder channelBuilder = SceneChannelBuilder
478                 .create(this.translationProvider,
479                         new ChannelGroupUID(thing.getUID(), HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES))
480                 .withScenes(scenes).withChannels(allChannels);
481
482         updateThing(editThing().withChannels(channelBuilder.build()).build());
483
484         createDeprecatedSceneChannels(scenes);
485
486         return scenes;
487     }
488
489     /**
490      * Create backwards compatible scene channels if any items configured before release 3.2
491      * are still linked. Users should have a reasonable amount of time to migrate to the new
492      * scene channels that are connected to a channel group.
493      */
494     private void createDeprecatedSceneChannels(List<Scene> scenes) {
495         if (deprecatedChannelsCreated) {
496             // Only do this once.
497             return;
498         }
499         ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
500                 HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES);
501         for (Scene scene : scenes) {
502             String channelId = Integer.toString(scene.id);
503             ChannelUID newChannelUid = new ChannelUID(channelGroupUid, channelId);
504             ChannelUID deprecatedChannelUid = new ChannelUID(getThing().getUID(), channelId);
505             String description = translationProvider.getText("dynamic-channel.scene-activate.deprecated.description",
506                     scene.getName());
507             Channel channel = ChannelBuilder.create(deprecatedChannelUid, CoreItemFactory.SWITCH)
508                     .withType(sceneChannelTypeUID).withLabel(scene.getName()).withDescription(description).build();
509             logger.debug("Creating deprecated channel '{}' ('{}') to probe for linked items", deprecatedChannelUid,
510                     scene.getName());
511             updateThing(editThing().withChannel(channel).build());
512             if (this.isLinked(deprecatedChannelUid) && !this.isLinked(newChannelUid)) {
513                 logger.warn("Created deprecated channel '{}' ('{}'), please link items to '{}' instead",
514                         deprecatedChannelUid, scene.getName(), newChannelUid);
515             } else {
516                 if (this.isLinked(newChannelUid)) {
517                     logger.debug("Removing deprecated channel '{}' ('{}') since new channel '{}' is linked",
518                             deprecatedChannelUid, scene.getName(), newChannelUid);
519
520                 } else {
521                     logger.debug("Removing deprecated channel '{}' ('{}') since it has no linked items",
522                             deprecatedChannelUid, scene.getName());
523                 }
524                 updateThing(editThing().withoutChannel(deprecatedChannelUid).build());
525             }
526         }
527         deprecatedChannelsCreated = true;
528     }
529
530     private List<SceneCollection> fetchSceneCollections()
531             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
532         SceneCollections sceneCollections = webTargets.getSceneCollections();
533         List<SceneCollection> sceneCollectionData = sceneCollections.sceneCollectionData;
534         if (sceneCollectionData == null) {
535             throw new HubInvalidResponseException("Missing 'sceneCollections.sceneCollectionData' element");
536         }
537         logger.debug("Received data for {} sceneCollections", sceneCollectionData.size());
538
539         return sceneCollectionData;
540     }
541
542     private List<SceneCollection> updateSceneGroupChannels()
543             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
544         List<SceneCollection> sceneCollections = fetchSceneCollections();
545
546         if (sceneCollections.size() == sceneCollectionCache.size()
547                 && sceneCollectionCache.containsAll(sceneCollections)) {
548             // Duplicates are not allowed. Reordering is not supported.
549             logger.debug("Preserving scene group channels, no changes detected");
550             return sceneCollections;
551         }
552
553         logger.debug("Updating all scene group channels, changes detected");
554         sceneCollectionCache = new CopyOnWriteArrayList<SceneCollection>(sceneCollections);
555
556         List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
557         allChannels
558                 .removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_SCENE_GROUPS.equals(c.getUID().getGroupId()));
559
560         SceneGroupChannelBuilder channelBuilder = SceneGroupChannelBuilder
561                 .create(this.translationProvider,
562                         new ChannelGroupUID(thing.getUID(), HDPowerViewBindingConstants.CHANNEL_GROUP_SCENE_GROUPS))
563                 .withSceneCollections(sceneCollections).withChannels(allChannels);
564
565         updateThing(editThing().withChannels(channelBuilder.build()).build());
566
567         return sceneCollections;
568     }
569
570     private List<ScheduledEvent> fetchScheduledEvents()
571             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
572         ScheduledEvents scheduledEvents = webTargets.getScheduledEvents();
573         List<ScheduledEvent> scheduledEventData = scheduledEvents.scheduledEventData;
574         if (scheduledEventData == null) {
575             throw new HubInvalidResponseException("Missing 'scheduledEvents.scheduledEventData' element");
576         }
577         logger.debug("Received data for {} scheduledEvents", scheduledEventData.size());
578
579         return scheduledEventData;
580     }
581
582     private List<ScheduledEvent> updateAutomationChannels(List<Scene> scenes, List<SceneCollection> sceneCollections)
583             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
584         List<ScheduledEvent> scheduledEvents = fetchScheduledEvents();
585
586         if (scheduledEvents.size() == scheduledEventCache.size() && scheduledEventCache.containsAll(scheduledEvents)) {
587             // Duplicates are not allowed. Reordering is not supported.
588             logger.debug("Preserving automation channels, no changes detected");
589             return scheduledEvents;
590         }
591
592         logger.debug("Updating all automation channels, changes detected");
593         scheduledEventCache = new CopyOnWriteArrayList<ScheduledEvent>(scheduledEvents);
594
595         List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
596         allChannels
597                 .removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS.equals(c.getUID().getGroupId()));
598         AutomationChannelBuilder channelBuilder = AutomationChannelBuilder
599                 .create(this.translationProvider,
600                         new ChannelGroupUID(thing.getUID(), HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS))
601                 .withScenes(scenes).withSceneCollections(sceneCollections).withScheduledEvents(scheduledEvents)
602                 .withChannels(allChannels);
603         updateThing(editThing().withChannels(channelBuilder.build()).build());
604
605         return scheduledEvents;
606     }
607
608     private void updateAutomationStates(List<ScheduledEvent> scheduledEvents) {
609         ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
610                 HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS);
611         for (ScheduledEvent scheduledEvent : scheduledEvents) {
612             String scheduledEventId = Integer.toString(scheduledEvent.id);
613             ChannelUID channelUid = new ChannelUID(channelGroupUid, scheduledEventId);
614             updateState(channelUid, scheduledEvent.enabled ? OnOffType.ON : OnOffType.OFF);
615         }
616     }
617
618     private Map<Thing, Integer> getShadeThingIdMap() {
619         Map<Thing, Integer> ret = new HashMap<>();
620         getThing().getThings().stream()
621                 .filter(thing -> HDPowerViewBindingConstants.THING_TYPE_SHADE.equals(thing.getThingTypeUID()))
622                 .forEach(thing -> {
623                     int id = thing.getConfiguration().as(HDPowerViewShadeConfiguration.class).id;
624                     if (id > 0) {
625                         ret.put(thing, id);
626                     }
627                 });
628         return ret;
629     }
630
631     private Map<Integer, ShadeData> getIdShadeDataMap(List<ShadeData> shadeData) {
632         Map<Integer, ShadeData> ret = new HashMap<>();
633         for (ShadeData shade : shadeData) {
634             if (shade.id > 0) {
635                 ret.put(shade.id, shade);
636             }
637         }
638         return ret;
639     }
640
641     private void requestRefreshShadePositions() {
642         Map<Thing, Integer> thingIdMap = getShadeThingIdMap();
643         for (Entry<Thing, Integer> item : thingIdMap.entrySet()) {
644             Thing thing = item.getKey();
645             if (thing.getStatusInfo().getStatusDetail() == ThingStatusDetail.GONE) {
646                 // Skip shades unknown to the Hub.
647                 logger.debug("Shade '{}' is unknown, skipping position refresh", item.getValue());
648                 continue;
649             }
650             ThingHandler handler = thing.getHandler();
651             if (handler instanceof HDPowerViewShadeHandler) {
652                 ((HDPowerViewShadeHandler) handler).requestRefreshShadePosition();
653             } else {
654                 int shadeId = item.getValue();
655                 logger.debug("Shade '{}' handler not initialized", shadeId);
656             }
657         }
658     }
659
660     private void requestRefreshShadeBatteryLevels() {
661         Map<Thing, Integer> thingIdMap = getShadeThingIdMap();
662         for (Entry<Thing, Integer> item : thingIdMap.entrySet()) {
663             Thing thing = item.getKey();
664             if (thing.getStatusInfo().getStatusDetail() == ThingStatusDetail.GONE) {
665                 // Skip shades unknown to the Hub.
666                 logger.debug("Shade '{}' is unknown, skipping battery level refresh", item.getValue());
667                 continue;
668             }
669             ThingHandler handler = thing.getHandler();
670             if (handler instanceof HDPowerViewShadeHandler) {
671                 ((HDPowerViewShadeHandler) handler).requestRefreshShadeBatteryLevel();
672             } else {
673                 int shadeId = item.getValue();
674                 logger.debug("Shade '{}' handler not initialized", shadeId);
675             }
676         }
677     }
678 }