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