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