]> git.basschouten.com Git - openhab-addons.git/blob
68065587dc15f63bae332531ebdfbd9a74c0d0ea
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.hdpowerview.internal.handler;
14
15 import java.time.Duration;
16 import java.time.Instant;
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Map.Entry;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.concurrent.CopyOnWriteArrayList;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
31 import org.openhab.binding.hdpowerview.internal.HDPowerViewTranslationProvider;
32 import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
33 import org.openhab.binding.hdpowerview.internal.builders.AutomationChannelBuilder;
34 import org.openhab.binding.hdpowerview.internal.builders.SceneChannelBuilder;
35 import org.openhab.binding.hdpowerview.internal.builders.SceneGroupChannelBuilder;
36 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewHubConfiguration;
37 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
38 import org.openhab.binding.hdpowerview.internal.dto.Firmware;
39 import org.openhab.binding.hdpowerview.internal.dto.HubFirmware;
40 import org.openhab.binding.hdpowerview.internal.dto.Scene;
41 import org.openhab.binding.hdpowerview.internal.dto.SceneCollection;
42 import org.openhab.binding.hdpowerview.internal.dto.ScheduledEvent;
43 import org.openhab.binding.hdpowerview.internal.dto.ShadeData;
44 import org.openhab.binding.hdpowerview.internal.dto.UserData;
45 import org.openhab.binding.hdpowerview.internal.dto.responses.SceneCollections;
46 import org.openhab.binding.hdpowerview.internal.dto.responses.Scenes;
47 import org.openhab.binding.hdpowerview.internal.dto.responses.ScheduledEvents;
48 import org.openhab.binding.hdpowerview.internal.dto.responses.Shades;
49 import org.openhab.binding.hdpowerview.internal.exceptions.HubException;
50 import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException;
51 import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException;
52 import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
53 import org.openhab.core.library.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.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     private final Duration firmwareVersionValidityPeriod = Duration.ofDays(1);
87
88     private long refreshInterval;
89     private long hardRefreshPositionInterval;
90     private long hardRefreshBatteryLevelInterval;
91
92     private @NonNullByDefault({}) 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 Instant userDataUpdated = Instant.MIN;
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             int id = Integer.parseInt(channelUID.getIdWithoutGroup());
132             if (sceneChannelTypeUID.equals(channel.getChannelTypeUID()) && OnOffType.ON == command) {
133                 webTargets.activateScene(id);
134                 // Reschedule soft poll for immediate shade position update.
135                 scheduleSoftPoll();
136             } else if (sceneGroupChannelTypeUID.equals(channel.getChannelTypeUID()) && OnOffType.ON == command) {
137                 webTargets.activateSceneCollection(id);
138                 // Reschedule soft poll for immediate shade position update.
139                 scheduleSoftPoll();
140             } else if (automationChannelTypeUID.equals(channel.getChannelTypeUID())) {
141                 webTargets.enableScheduledEvent(id, OnOffType.ON == command);
142             }
143         } catch (HubMaintenanceException e) {
144             // exceptions are logged in HDPowerViewWebTargets
145             userDataUpdated = Instant.MIN;
146         } catch (NumberFormatException | HubException e) {
147             logger.debug("Unexpected error {}", e.getMessage());
148         }
149     }
150
151     @Override
152     public void initialize() {
153         logger.debug("Initializing hub");
154         HDPowerViewHubConfiguration config = getConfigAs(HDPowerViewHubConfiguration.class);
155         String host = config.host;
156
157         if (host == null || host.isEmpty()) {
158             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
159                     "@text/offline.conf-error.no-host-address");
160             return;
161         }
162
163         pendingShadeInitializations.clear();
164         webTargets = new HDPowerViewWebTargets(httpClient, host);
165         refreshInterval = config.refresh;
166         hardRefreshPositionInterval = config.hardRefresh;
167         hardRefreshBatteryLevelInterval = config.hardRefreshBatteryLevel;
168         initializeChannels();
169         userDataUpdated = Instant.MIN;
170
171         updateStatus(ThingStatus.UNKNOWN);
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     }
182
183     public HDPowerViewWebTargets getWebTargets() {
184         return webTargets;
185     }
186
187     @Override
188     public void handleRemoval() {
189         super.handleRemoval();
190         stopPoll();
191     }
192
193     @Override
194     public void dispose() {
195         super.dispose();
196         stopPoll();
197         pendingShadeInitializations.clear();
198     }
199
200     @Override
201     public void childHandlerInitialized(final ThingHandler childHandler, final Thing childThing) {
202         logger.debug("Child handler initialized: {}", childThing.getUID());
203         if (childHandler instanceof HDPowerViewShadeHandler) {
204             ShadeData shadeData = pendingShadeInitializations.remove(childThing.getUID());
205             if (shadeData != null) {
206                 if (shadeData.id > 0) {
207                     updateShadeThing(shadeData.id, childThing, shadeData);
208                 } else {
209                     updateUnknownShadeThing(childThing);
210                 }
211             }
212         }
213         super.childHandlerInitialized(childHandler, childThing);
214     }
215
216     @Override
217     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
218         logger.debug("Child handler disposed: {}", childThing.getUID());
219         if (childHandler instanceof HDPowerViewShadeHandler) {
220             pendingShadeInitializations.remove(childThing.getUID());
221         }
222         super.childHandlerDisposed(childHandler, childThing);
223     }
224
225     private void schedulePoll() {
226         scheduleSoftPoll();
227         scheduleHardPoll();
228     }
229
230     private void scheduleSoftPoll() {
231         ScheduledFuture<?> future = this.pollFuture;
232         if (future != null) {
233             future.cancel(false);
234         }
235         logger.debug("Scheduling poll every {} ms", refreshInterval);
236         this.pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, refreshInterval, TimeUnit.MILLISECONDS);
237     }
238
239     private void scheduleHardPoll() {
240         ScheduledFuture<?> future = this.hardRefreshPositionFuture;
241         if (future != null) {
242             future.cancel(false);
243         }
244         if (hardRefreshPositionInterval > 0) {
245             logger.debug("Scheduling hard position refresh every {} minutes", hardRefreshPositionInterval);
246             this.hardRefreshPositionFuture = scheduler.scheduleWithFixedDelay(this::requestRefreshShadePositions, 1,
247                     hardRefreshPositionInterval, TimeUnit.MINUTES);
248         }
249
250         future = this.hardRefreshBatteryLevelFuture;
251         if (future != null) {
252             future.cancel(false);
253         }
254         if (hardRefreshBatteryLevelInterval > 0) {
255             logger.debug("Scheduling hard battery level refresh every {} hours", hardRefreshBatteryLevelInterval);
256             this.hardRefreshBatteryLevelFuture = scheduler.scheduleWithFixedDelay(
257                     this::requestRefreshShadeBatteryLevels, 1, hardRefreshBatteryLevelInterval, TimeUnit.HOURS);
258         }
259     }
260
261     private synchronized void stopPoll() {
262         ScheduledFuture<?> future = this.pollFuture;
263         if (future != null) {
264             future.cancel(true);
265         }
266         this.pollFuture = null;
267
268         future = this.hardRefreshPositionFuture;
269         if (future != null) {
270             future.cancel(true);
271         }
272         this.hardRefreshPositionFuture = null;
273
274         future = this.hardRefreshBatteryLevelFuture;
275         if (future != null) {
276             future.cancel(true);
277         }
278         this.hardRefreshBatteryLevelFuture = null;
279     }
280
281     private synchronized void poll() {
282         try {
283             updateUserDataProperties();
284         } catch (HubException e) {
285             logger.warn("Failed to update firmware properties: {}", e.getMessage());
286         }
287
288         try {
289             logger.debug("Polling for state");
290             pollShades();
291
292             List<Scene> scenes = updateSceneChannels();
293             List<SceneCollection> sceneCollections = updateSceneGroupChannels();
294             List<ScheduledEvent> scheduledEvents = updateAutomationChannels(scenes, sceneCollections);
295
296             // Scheduled events should also have their current state updated if event has been
297             // enabled or disabled through app or other integration.
298             updateAutomationStates(scheduledEvents);
299         } catch (HubInvalidResponseException e) {
300             Throwable cause = e.getCause();
301             if (cause == null) {
302                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
303             } else {
304                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
305             }
306         } catch (HubMaintenanceException e) {
307             // exceptions are logged in HDPowerViewWebTargets
308             userDataUpdated = Instant.MIN;
309         } catch (HubException e) {
310             logger.warn("Error connecting to bridge: {}", e.getMessage());
311             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
312             userDataUpdated = Instant.MIN;
313         }
314     }
315
316     private void updateUserDataProperties()
317             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
318         if (userDataUpdated.isAfter(Instant.now().minus(firmwareVersionValidityPeriod))) {
319             return;
320         }
321
322         UserData userData = webTargets.getUserData();
323         Map<String, String> properties = editProperties();
324         HubFirmware firmwareVersions = userData.firmware;
325         if (firmwareVersions != null) {
326             updateFirmwareProperties(properties, firmwareVersions);
327         }
328         String serialNumber = userData.serialNumber;
329         if (serialNumber != null) {
330             properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
331         }
332         String macAddress = userData.macAddress;
333         if (macAddress != null) {
334             properties.put(Thing.PROPERTY_MAC_ADDRESS, macAddress);
335         }
336         String hubName = userData.getHubName();
337         if (!hubName.isEmpty()) {
338             properties.put(HDPowerViewBindingConstants.PROPERTY_HUB_NAME, hubName);
339         }
340         updateProperties(properties);
341         userDataUpdated = Instant.now();
342     }
343
344     private void updateFirmwareProperties(Map<String, String> properties, HubFirmware firmwareVersions) {
345         Firmware mainProcessor = firmwareVersions.mainProcessor;
346         if (mainProcessor == null) {
347             logger.warn("Main processor firmware version missing in response.");
348             return;
349         }
350         logger.debug("Main processor firmware version received: {}, {}", mainProcessor.name, mainProcessor.toString());
351         String mainProcessorName = mainProcessor.name;
352         if (mainProcessorName != null) {
353             properties.put(HDPowerViewBindingConstants.PROPERTY_FIRMWARE_NAME, mainProcessorName);
354         }
355         properties.put(Thing.PROPERTY_FIRMWARE_VERSION, mainProcessor.toString());
356         Firmware radio = firmwareVersions.radio;
357         if (radio != null) {
358             logger.debug("Radio firmware version received: {}", radio.toString());
359             properties.put(HDPowerViewBindingConstants.PROPERTY_RADIO_FIRMWARE_VERSION, radio.toString());
360         }
361     }
362
363     private void pollShades() throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
364         Shades shades = webTargets.getShades();
365         List<ShadeData> shadesData = shades.shadeData;
366         if (shadesData == null) {
367             throw new HubInvalidResponseException("Missing 'shades.shadeData' element");
368         }
369
370         updateStatus(ThingStatus.ONLINE);
371         logger.debug("Received data for {} shades", shadesData.size());
372
373         Map<Integer, ShadeData> idShadeDataMap = getIdShadeDataMap(shadesData);
374         Map<Thing, Integer> thingIdMap = getShadeThingIdMap();
375         for (Entry<Thing, Integer> item : thingIdMap.entrySet()) {
376             Thing thing = item.getKey();
377             int shadeId = item.getValue();
378             ShadeData shadeData = idShadeDataMap.get(shadeId);
379             if (shadeData != null) {
380                 updateShadeThing(shadeId, thing, shadeData);
381             } else {
382                 updateUnknownShadeThing(thing);
383             }
384         }
385     }
386
387     private void updateShadeThing(int shadeId, Thing thing, ShadeData shadeData) {
388         HDPowerViewShadeHandler thingHandler = ((HDPowerViewShadeHandler) thing.getHandler());
389         if (thingHandler == null) {
390             logger.debug("Shade '{}' handler not initialized", shadeId);
391             pendingShadeInitializations.put(thing.getUID(), shadeData);
392             return;
393         }
394         ThingStatus thingStatus = thingHandler.getThing().getStatus();
395         switch (thingStatus) {
396             case UNKNOWN:
397             case ONLINE:
398             case OFFLINE:
399                 logger.debug("Updating shade '{}'", shadeId);
400                 thingHandler.onReceiveUpdate(shadeData);
401                 break;
402             case UNINITIALIZED:
403             case INITIALIZING:
404                 logger.debug("Shade '{}' handler not yet ready; status: {}", shadeId, thingStatus);
405                 pendingShadeInitializations.put(thing.getUID(), shadeData);
406                 break;
407             case REMOVING:
408             case REMOVED:
409             default:
410                 logger.debug("Ignoring shade update for shade '{}' in status {}", shadeId, thingStatus);
411                 break;
412         }
413     }
414
415     private void updateUnknownShadeThing(Thing thing) {
416         String shadeId = thing.getUID().getId();
417         logger.debug("Shade '{}' has no data in hub", shadeId);
418         HDPowerViewShadeHandler thingHandler = ((HDPowerViewShadeHandler) thing.getHandler());
419         if (thingHandler == null) {
420             logger.debug("Shade '{}' handler not initialized", shadeId);
421             pendingShadeInitializations.put(thing.getUID(), new ShadeData());
422             return;
423         }
424         ThingStatus thingStatus = thingHandler.getThing().getStatus();
425         switch (thingStatus) {
426             case UNKNOWN:
427             case ONLINE:
428             case OFFLINE:
429                 thing.setStatusInfo(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.GONE,
430                         "@text/offline.gone.shade-unknown-to-hub"));
431                 break;
432             case UNINITIALIZED:
433             case INITIALIZING:
434                 logger.debug("Shade '{}' handler not yet ready; status: {}", shadeId, thingStatus);
435                 pendingShadeInitializations.put(thing.getUID(), new ShadeData());
436                 break;
437             case REMOVING:
438             case REMOVED:
439             default:
440                 logger.debug("Ignoring shade status update for shade '{}' in status {}", shadeId, thingStatus);
441                 break;
442         }
443     }
444
445     private List<Scene> fetchScenes()
446             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
447         Scenes scenes = webTargets.getScenes();
448         List<Scene> sceneData = scenes.sceneData;
449         if (sceneData == null) {
450             throw new HubInvalidResponseException("Missing 'scenes.sceneData' element");
451         }
452         logger.debug("Received data for {} scenes", sceneData.size());
453
454         return sceneData;
455     }
456
457     private List<Scene> updateSceneChannels()
458             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
459         List<Scene> scenes = fetchScenes();
460
461         if (scenes.size() == sceneCache.size() && sceneCache.containsAll(scenes)) {
462             // Duplicates are not allowed. Reordering is not supported.
463             logger.debug("Preserving scene channels, no changes detected");
464             return scenes;
465         }
466
467         logger.debug("Updating all scene channels, changes detected");
468         sceneCache = new CopyOnWriteArrayList<Scene>(scenes);
469
470         List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
471         allChannels.removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES.equals(c.getUID().getGroupId()));
472
473         SceneChannelBuilder channelBuilder = SceneChannelBuilder
474                 .create(this.translationProvider,
475                         new ChannelGroupUID(thing.getUID(), HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES))
476                 .withScenes(scenes).withChannels(allChannels);
477
478         updateThing(editThing().withChannels(channelBuilder.build()).build());
479
480         return scenes;
481     }
482
483     private List<SceneCollection> fetchSceneCollections()
484             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
485         SceneCollections sceneCollections = webTargets.getSceneCollections();
486         List<SceneCollection> sceneCollectionData = sceneCollections.sceneCollectionData;
487         if (sceneCollectionData == null) {
488             throw new HubInvalidResponseException("Missing 'sceneCollections.sceneCollectionData' element");
489         }
490         logger.debug("Received data for {} sceneCollections", sceneCollectionData.size());
491
492         return sceneCollectionData;
493     }
494
495     private List<SceneCollection> updateSceneGroupChannels()
496             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
497         List<SceneCollection> sceneCollections = fetchSceneCollections();
498
499         if (sceneCollections.size() == sceneCollectionCache.size()
500                 && sceneCollectionCache.containsAll(sceneCollections)) {
501             // Duplicates are not allowed. Reordering is not supported.
502             logger.debug("Preserving scene group channels, no changes detected");
503             return sceneCollections;
504         }
505
506         logger.debug("Updating all scene group channels, changes detected");
507         sceneCollectionCache = new CopyOnWriteArrayList<SceneCollection>(sceneCollections);
508
509         List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
510         allChannels
511                 .removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_SCENE_GROUPS.equals(c.getUID().getGroupId()));
512
513         SceneGroupChannelBuilder channelBuilder = SceneGroupChannelBuilder
514                 .create(this.translationProvider,
515                         new ChannelGroupUID(thing.getUID(), HDPowerViewBindingConstants.CHANNEL_GROUP_SCENE_GROUPS))
516                 .withSceneCollections(sceneCollections).withChannels(allChannels);
517
518         updateThing(editThing().withChannels(channelBuilder.build()).build());
519
520         return sceneCollections;
521     }
522
523     private List<ScheduledEvent> fetchScheduledEvents()
524             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
525         ScheduledEvents scheduledEvents = webTargets.getScheduledEvents();
526         List<ScheduledEvent> scheduledEventData = scheduledEvents.scheduledEventData;
527         if (scheduledEventData == null) {
528             throw new HubInvalidResponseException("Missing 'scheduledEvents.scheduledEventData' element");
529         }
530         logger.debug("Received data for {} scheduledEvents", scheduledEventData.size());
531
532         return scheduledEventData;
533     }
534
535     private List<ScheduledEvent> updateAutomationChannels(List<Scene> scenes, List<SceneCollection> sceneCollections)
536             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
537         List<ScheduledEvent> scheduledEvents = fetchScheduledEvents();
538
539         if (scheduledEvents.size() == scheduledEventCache.size() && scheduledEventCache.containsAll(scheduledEvents)) {
540             // Duplicates are not allowed. Reordering is not supported.
541             logger.debug("Preserving automation channels, no changes detected");
542             return scheduledEvents;
543         }
544
545         logger.debug("Updating all automation channels, changes detected");
546         scheduledEventCache = new CopyOnWriteArrayList<ScheduledEvent>(scheduledEvents);
547
548         List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
549         allChannels
550                 .removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS.equals(c.getUID().getGroupId()));
551         AutomationChannelBuilder channelBuilder = AutomationChannelBuilder
552                 .create(this.translationProvider,
553                         new ChannelGroupUID(thing.getUID(), HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS))
554                 .withScenes(scenes).withSceneCollections(sceneCollections).withScheduledEvents(scheduledEvents)
555                 .withChannels(allChannels);
556         updateThing(editThing().withChannels(channelBuilder.build()).build());
557
558         return scheduledEvents;
559     }
560
561     private void updateAutomationStates(List<ScheduledEvent> scheduledEvents) {
562         ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
563                 HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS);
564         for (ScheduledEvent scheduledEvent : scheduledEvents) {
565             String scheduledEventId = Integer.toString(scheduledEvent.id);
566             ChannelUID channelUid = new ChannelUID(channelGroupUid, scheduledEventId);
567             updateState(channelUid, scheduledEvent.enabled ? OnOffType.ON : OnOffType.OFF);
568         }
569     }
570
571     private Map<Thing, Integer> getShadeThingIdMap() {
572         Map<Thing, Integer> ret = new HashMap<>();
573         getThing().getThings().stream()
574                 .filter(thing -> HDPowerViewBindingConstants.THING_TYPE_SHADE.equals(thing.getThingTypeUID()))
575                 .forEach(thing -> {
576                     int id = thing.getConfiguration().as(HDPowerViewShadeConfiguration.class).id;
577                     if (id > 0) {
578                         ret.put(thing, id);
579                     }
580                 });
581         return ret;
582     }
583
584     private Map<Integer, ShadeData> getIdShadeDataMap(List<ShadeData> shadeData) {
585         Map<Integer, ShadeData> ret = new HashMap<>();
586         for (ShadeData shade : shadeData) {
587             if (shade.id > 0) {
588                 ret.put(shade.id, shade);
589             }
590         }
591         return ret;
592     }
593
594     private void requestRefreshShadePositions() {
595         Map<Thing, Integer> thingIdMap = getShadeThingIdMap();
596         for (Entry<Thing, Integer> item : thingIdMap.entrySet()) {
597             Thing thing = item.getKey();
598             if (thing.getStatusInfo().getStatusDetail() == ThingStatusDetail.GONE) {
599                 // Skip shades unknown to the Hub.
600                 logger.debug("Shade '{}' is unknown, skipping position refresh", item.getValue());
601                 continue;
602             }
603             ThingHandler handler = thing.getHandler();
604             if (handler instanceof HDPowerViewShadeHandler) {
605                 ((HDPowerViewShadeHandler) handler).requestRefreshShadePosition();
606             } else {
607                 int shadeId = item.getValue();
608                 logger.debug("Shade '{}' handler not initialized", shadeId);
609             }
610         }
611     }
612
613     private void requestRefreshShadeBatteryLevels() {
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 battery level refresh", item.getValue());
620                 continue;
621             }
622             ThingHandler handler = thing.getHandler();
623             if (handler instanceof HDPowerViewShadeHandler) {
624                 ((HDPowerViewShadeHandler) handler).requestRefreshShadeBatteryLevel();
625             } else {
626                 int shadeId = item.getValue();
627                 logger.debug("Shade '{}' handler not initialized", shadeId);
628             }
629         }
630     }
631 }