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