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