2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.hdpowerview.internal.handler;
15 import java.time.DayOfWeek;
16 import java.time.LocalTime;
17 import java.time.format.TextStyle;
18 import java.util.ArrayList;
19 import java.util.EnumSet;
20 import java.util.HashMap;
21 import java.util.List;
23 import java.util.Map.Entry;
24 import java.util.StringJoiner;
25 import java.util.concurrent.CopyOnWriteArrayList;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
29 import javax.ws.rs.ProcessingException;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.eclipse.jetty.client.HttpClient;
34 import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
35 import org.openhab.binding.hdpowerview.internal.HDPowerViewTranslationProvider;
36 import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
37 import org.openhab.binding.hdpowerview.internal.HubMaintenanceException;
38 import org.openhab.binding.hdpowerview.internal.HubProcessingException;
39 import org.openhab.binding.hdpowerview.internal.api.Firmware;
40 import org.openhab.binding.hdpowerview.internal.api.responses.FirmwareVersion;
41 import org.openhab.binding.hdpowerview.internal.api.responses.FirmwareVersions;
42 import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections;
43 import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection;
44 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
45 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene;
46 import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents;
47 import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents.ScheduledEvent;
48 import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
49 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
50 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewHubConfiguration;
51 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
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.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;
70 import com.google.gson.JsonParseException;
73 * The {@link HDPowerViewHubHandler} is responsible for handling commands, which
74 * are sent to one of the channels.
76 * @author Andy Lintner - Initial contribution
77 * @author Andrew Fiddian-Green - Added support for secondary rail positions
78 * @author Jacob Laursen - Added support for scene groups and automations
81 public class HDPowerViewHubHandler extends BaseBridgeHandler {
83 private static final long INITIAL_SOFT_POLL_DELAY_MS = 5_000;
85 private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubHandler.class);
86 private final HttpClient httpClient;
87 private final HDPowerViewTranslationProvider translationProvider;
89 private long refreshInterval;
90 private long hardRefreshPositionInterval;
91 private long hardRefreshBatteryLevelInterval;
93 private @Nullable HDPowerViewWebTargets webTargets;
94 private @Nullable ScheduledFuture<?> pollFuture;
95 private @Nullable ScheduledFuture<?> hardRefreshPositionFuture;
96 private @Nullable ScheduledFuture<?> hardRefreshBatteryLevelFuture;
98 private List<Scene> sceneCache = new CopyOnWriteArrayList<>();
99 private List<SceneCollection> sceneCollectionCache = new CopyOnWriteArrayList<>();
100 private List<ScheduledEvent> scheduledEventCache = new CopyOnWriteArrayList<>();
101 private @Nullable FirmwareVersions firmwareVersions;
102 private Boolean deprecatedChannelsCreated = false;
104 private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
105 HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE);
107 private final ChannelTypeUID sceneGroupChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
108 HDPowerViewBindingConstants.CHANNELTYPE_SCENE_GROUP_ACTIVATE);
110 private final ChannelTypeUID automationChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
111 HDPowerViewBindingConstants.CHANNELTYPE_AUTOMATION_ENABLED);
113 public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient,
114 HDPowerViewTranslationProvider translationProvider) {
116 this.httpClient = httpClient;
117 this.translationProvider = translationProvider;
121 public void handleCommand(ChannelUID channelUID, Command command) {
122 if (RefreshType.REFRESH == command) {
123 requestRefreshShadePositions();
127 Channel channel = getThing().getChannel(channelUID.getId());
128 if (channel == null) {
133 HDPowerViewWebTargets webTargets = this.webTargets;
134 if (webTargets == null) {
135 throw new ProcessingException("Web targets not initialized");
137 int id = Integer.parseInt(channelUID.getIdWithoutGroup());
138 if (sceneChannelTypeUID.equals(channel.getChannelTypeUID()) && OnOffType.ON == command) {
139 webTargets.activateScene(id);
140 // Reschedule soft poll for immediate shade position update.
142 } else if (sceneGroupChannelTypeUID.equals(channel.getChannelTypeUID()) && OnOffType.ON == command) {
143 webTargets.activateSceneCollection(id);
144 // Reschedule soft poll for immediate shade position update.
146 } else if (automationChannelTypeUID.equals(channel.getChannelTypeUID())) {
147 webTargets.enableScheduledEvent(id, OnOffType.ON == command);
149 } catch (HubMaintenanceException e) {
150 // exceptions are logged in HDPowerViewWebTargets
151 } catch (NumberFormatException | HubProcessingException e) {
152 logger.debug("Unexpected error {}", e.getMessage());
157 public void initialize() {
158 logger.debug("Initializing hub");
159 HDPowerViewHubConfiguration config = getConfigAs(HDPowerViewHubConfiguration.class);
160 String host = config.host;
162 if (host == null || host.isEmpty()) {
163 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
164 "@text/offline.conf-error.no-host-address");
168 webTargets = new HDPowerViewWebTargets(httpClient, host);
169 refreshInterval = config.refresh;
170 hardRefreshPositionInterval = config.hardRefresh;
171 hardRefreshBatteryLevelInterval = config.hardRefreshBatteryLevel;
172 initializeChannels();
176 private void initializeChannels() {
177 // Rebuild dynamic channels and synchronize with cache.
178 updateThing(editThing().withChannels(new ArrayList<Channel>()).build());
180 sceneCollectionCache.clear();
181 scheduledEventCache.clear();
182 deprecatedChannelsCreated = false;
185 public @Nullable HDPowerViewWebTargets getWebTargets() {
190 public void handleRemoval() {
191 super.handleRemoval();
196 public void dispose() {
201 private void schedulePoll() {
202 scheduleSoftPoll(INITIAL_SOFT_POLL_DELAY_MS);
206 private void scheduleSoftPoll(long initialDelay) {
207 ScheduledFuture<?> future = this.pollFuture;
208 if (future != null) {
209 future.cancel(false);
211 logger.debug("Scheduling poll for {} ms out, then every {} ms", initialDelay, refreshInterval);
212 this.pollFuture = scheduler.scheduleWithFixedDelay(this::poll, initialDelay, refreshInterval,
213 TimeUnit.MILLISECONDS);
216 private void scheduleHardPoll() {
217 ScheduledFuture<?> future = this.hardRefreshPositionFuture;
218 if (future != null) {
219 future.cancel(false);
221 if (hardRefreshPositionInterval > 0) {
222 logger.debug("Scheduling hard position refresh every {} minutes", hardRefreshPositionInterval);
223 this.hardRefreshPositionFuture = scheduler.scheduleWithFixedDelay(this::requestRefreshShadePositions, 1,
224 hardRefreshPositionInterval, TimeUnit.MINUTES);
227 future = this.hardRefreshBatteryLevelFuture;
228 if (future != null) {
229 future.cancel(false);
231 if (hardRefreshBatteryLevelInterval > 0) {
232 logger.debug("Scheduling hard battery level refresh every {} hours", hardRefreshBatteryLevelInterval);
233 this.hardRefreshBatteryLevelFuture = scheduler.scheduleWithFixedDelay(
234 this::requestRefreshShadeBatteryLevels, 1, hardRefreshBatteryLevelInterval, TimeUnit.HOURS);
238 private synchronized void stopPoll() {
239 ScheduledFuture<?> future = this.pollFuture;
240 if (future != null) {
243 this.pollFuture = null;
245 future = this.hardRefreshPositionFuture;
246 if (future != null) {
249 this.hardRefreshPositionFuture = null;
251 future = this.hardRefreshBatteryLevelFuture;
252 if (future != null) {
255 this.hardRefreshBatteryLevelFuture = null;
258 private synchronized void poll() {
260 logger.debug("Polling for state");
261 updateFirmwareProperties();
264 List<Scene> scenes = updateSceneChannels();
265 List<SceneCollection> sceneCollections = updateSceneCollectionChannels();
266 List<ScheduledEvent> scheduledEvents = updateScheduledEventChannels(scenes, sceneCollections);
268 // Scheduled events should also have their current state updated if event has been
269 // enabled or disabled through app or other integration.
270 updateScheduledEventStates(scheduledEvents);
271 } catch (JsonParseException e) {
272 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
273 } catch (HubProcessingException e) {
274 logger.warn("Error connecting to bridge: {}", e.getMessage());
275 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, e.getMessage());
276 } catch (HubMaintenanceException e) {
277 // exceptions are logged in HDPowerViewWebTargets
281 private void updateFirmwareProperties() throws JsonParseException, HubProcessingException, HubMaintenanceException {
282 if (firmwareVersions != null) {
285 HDPowerViewWebTargets webTargets = this.webTargets;
286 if (webTargets == null) {
287 throw new ProcessingException("Web targets not initialized");
289 FirmwareVersion firmwareVersion = webTargets.getFirmwareVersion();
290 if (firmwareVersion == null || firmwareVersion.firmware == null) {
291 logger.warn("Unable to get firmware version.");
294 this.firmwareVersions = firmwareVersion.firmware;
295 Firmware mainProcessor = firmwareVersion.firmware.mainProcessor;
296 if (mainProcessor == null) {
297 logger.warn("Main processor firmware version missing in response.");
300 logger.debug("Main processor firmware version received: {}, {}", mainProcessor.name, mainProcessor.toString());
301 Map<String, String> properties = editProperties();
302 if (mainProcessor.name != null) {
303 properties.put(HDPowerViewBindingConstants.PROPERTY_FIRMWARE_NAME, mainProcessor.name);
305 properties.put(HDPowerViewBindingConstants.PROPERTY_FIRMWARE_VERSION, mainProcessor.toString());
306 Firmware radio = firmwareVersion.firmware.radio;
308 logger.debug("Radio firmware version received: {}", radio.toString());
309 properties.put(HDPowerViewBindingConstants.PROPERTY_RADIO_FIRMWARE_VERSION, radio.toString());
311 updateProperties(properties);
314 private void pollShades() throws JsonParseException, HubProcessingException, HubMaintenanceException {
315 HDPowerViewWebTargets webTargets = this.webTargets;
316 if (webTargets == null) {
317 throw new ProcessingException("Web targets not initialized");
320 Shades shades = webTargets.getShades();
321 if (shades == null) {
322 throw new JsonParseException("Missing 'shades' element");
325 List<ShadeData> shadesData = shades.shadeData;
326 if (shadesData == null) {
327 throw new JsonParseException("Missing 'shades.shadeData' element");
330 updateStatus(ThingStatus.ONLINE);
331 logger.debug("Received data for {} shades", shadesData.size());
333 Map<String, ShadeData> idShadeDataMap = getIdShadeDataMap(shadesData);
334 Map<Thing, String> thingIdMap = getThingIdMap();
335 for (Entry<Thing, String> item : thingIdMap.entrySet()) {
336 Thing thing = item.getKey();
337 String shadeId = item.getValue();
338 ShadeData shadeData = idShadeDataMap.get(shadeId);
339 updateShadeThing(shadeId, thing, shadeData);
343 private void updateShadeThing(String shadeId, Thing thing, @Nullable ShadeData shadeData) {
344 HDPowerViewShadeHandler thingHandler = ((HDPowerViewShadeHandler) thing.getHandler());
345 if (thingHandler == null) {
346 logger.debug("Shade '{}' handler not initialized", shadeId);
349 if (shadeData == null) {
350 logger.debug("Shade '{}' has no data in hub", shadeId);
352 logger.debug("Updating shade '{}'", shadeId);
354 thingHandler.onReceiveUpdate(shadeData);
357 private List<Scene> fetchScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException {
358 HDPowerViewWebTargets webTargets = this.webTargets;
359 if (webTargets == null) {
360 throw new ProcessingException("Web targets not initialized");
363 Scenes scenes = webTargets.getScenes();
364 if (scenes == null) {
365 throw new JsonParseException("Missing 'scenes' element");
368 List<Scene> sceneData = scenes.sceneData;
369 if (sceneData == null) {
370 throw new JsonParseException("Missing 'scenes.sceneData' element");
372 logger.debug("Received data for {} scenes", sceneData.size());
377 private List<Scene> updateSceneChannels()
378 throws JsonParseException, HubProcessingException, HubMaintenanceException {
379 List<Scene> scenes = fetchScenes();
381 if (scenes.size() == sceneCache.size() && sceneCache.containsAll(scenes)) {
382 // Duplicates are not allowed. Reordering is not supported.
383 logger.debug("Preserving scene channels, no changes detected");
387 logger.debug("Updating all scene channels, changes detected");
388 sceneCache = new CopyOnWriteArrayList<Scene>(scenes);
390 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
391 allChannels.removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES.equals(c.getUID().getGroupId()));
392 scenes.stream().sorted().forEach(scene -> allChannels.add(createSceneChannel(scene)));
393 updateThing(editThing().withChannels(allChannels).build());
395 createDeprecatedSceneChannels(scenes);
400 private Channel createSceneChannel(Scene scene) {
401 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
402 HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES);
403 ChannelUID channelUid = new ChannelUID(channelGroupUid, Integer.toString(scene.id));
404 String description = translationProvider.getText("dynamic-channel.scene-activate.description", scene.getName());
405 Channel channel = ChannelBuilder.create(channelUid, CoreItemFactory.SWITCH).withType(sceneChannelTypeUID)
406 .withLabel(scene.getName()).withDescription(description).build();
412 * Create backwards compatible scene channels if any items configured before release 3.2
413 * are still linked. Users should have a reasonable amount of time to migrate to the new
414 * scene channels that are connected to a channel group.
416 private void createDeprecatedSceneChannels(List<Scene> scenes) {
417 if (deprecatedChannelsCreated) {
418 // Only do this once.
421 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
422 HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES);
423 for (Scene scene : scenes) {
424 String channelId = Integer.toString(scene.id);
425 ChannelUID newChannelUid = new ChannelUID(channelGroupUid, channelId);
426 ChannelUID deprecatedChannelUid = new ChannelUID(getThing().getUID(), channelId);
427 String description = translationProvider.getText("dynamic-channel.scene-activate.deprecated.description",
429 Channel channel = ChannelBuilder.create(deprecatedChannelUid, CoreItemFactory.SWITCH)
430 .withType(sceneChannelTypeUID).withLabel(scene.getName()).withDescription(description).build();
431 logger.debug("Creating deprecated channel '{}' ('{}') to probe for linked items", deprecatedChannelUid,
433 updateThing(editThing().withChannel(channel).build());
434 if (this.isLinked(deprecatedChannelUid) && !this.isLinked(newChannelUid)) {
435 logger.warn("Created deprecated channel '{}' ('{}'), please link items to '{}' instead",
436 deprecatedChannelUid, scene.getName(), newChannelUid);
438 if (this.isLinked(newChannelUid)) {
439 logger.debug("Removing deprecated channel '{}' ('{}') since new channel '{}' is linked",
440 deprecatedChannelUid, scene.getName(), newChannelUid);
443 logger.debug("Removing deprecated channel '{}' ('{}') since it has no linked items",
444 deprecatedChannelUid, scene.getName());
446 updateThing(editThing().withoutChannel(deprecatedChannelUid).build());
449 deprecatedChannelsCreated = true;
452 private List<SceneCollection> fetchSceneCollections()
453 throws JsonParseException, HubProcessingException, HubMaintenanceException {
454 HDPowerViewWebTargets webTargets = this.webTargets;
455 if (webTargets == null) {
456 throw new ProcessingException("Web targets not initialized");
459 SceneCollections sceneCollections = webTargets.getSceneCollections();
460 if (sceneCollections == null) {
461 throw new JsonParseException("Missing 'sceneCollections' element");
464 List<SceneCollection> sceneCollectionData = sceneCollections.sceneCollectionData;
465 if (sceneCollectionData == null) {
466 throw new JsonParseException("Missing 'sceneCollections.sceneCollectionData' element");
468 logger.debug("Received data for {} sceneCollections", sceneCollectionData.size());
470 return sceneCollectionData;
473 private List<SceneCollection> updateSceneCollectionChannels()
474 throws JsonParseException, HubProcessingException, HubMaintenanceException {
475 List<SceneCollection> sceneCollections = fetchSceneCollections();
477 if (sceneCollections.size() == sceneCollectionCache.size()
478 && sceneCollectionCache.containsAll(sceneCollections)) {
479 // Duplicates are not allowed. Reordering is not supported.
480 logger.debug("Preserving scene collection channels, no changes detected");
481 return sceneCollections;
484 logger.debug("Updating all scene collection channels, changes detected");
485 sceneCollectionCache = new CopyOnWriteArrayList<SceneCollection>(sceneCollections);
487 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
489 .removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_SCENE_GROUPS.equals(c.getUID().getGroupId()));
490 sceneCollections.stream().sorted()
491 .forEach(sceneCollection -> allChannels.add(createSceneCollectionChannel(sceneCollection)));
492 updateThing(editThing().withChannels(allChannels).build());
494 return sceneCollections;
497 private Channel createSceneCollectionChannel(SceneCollection sceneCollection) {
498 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
499 HDPowerViewBindingConstants.CHANNEL_GROUP_SCENE_GROUPS);
500 ChannelUID channelUid = new ChannelUID(channelGroupUid, Integer.toString(sceneCollection.id));
501 String description = translationProvider.getText("dynamic-channel.scene-group-activate.description",
502 sceneCollection.getName());
503 Channel channel = ChannelBuilder.create(channelUid, CoreItemFactory.SWITCH).withType(sceneGroupChannelTypeUID)
504 .withLabel(sceneCollection.getName()).withDescription(description).build();
509 private List<ScheduledEvent> fetchScheduledEvents()
510 throws JsonParseException, HubProcessingException, HubMaintenanceException {
511 HDPowerViewWebTargets webTargets = this.webTargets;
512 if (webTargets == null) {
513 throw new ProcessingException("Web targets not initialized");
516 ScheduledEvents scheduledEvents = webTargets.getScheduledEvents();
517 if (scheduledEvents == null) {
518 throw new JsonParseException("Missing 'scheduledEvents' element");
521 List<ScheduledEvent> scheduledEventData = scheduledEvents.scheduledEventData;
522 if (scheduledEventData == null) {
523 throw new JsonParseException("Missing 'scheduledEvents.scheduledEventData' element");
525 logger.debug("Received data for {} scheduledEvents", scheduledEventData.size());
527 return scheduledEventData;
530 private List<ScheduledEvent> updateScheduledEventChannels(List<Scene> scenes,
531 List<SceneCollection> sceneCollections)
532 throws JsonParseException, HubProcessingException, HubMaintenanceException {
533 List<ScheduledEvent> scheduledEvents = fetchScheduledEvents();
535 if (scheduledEvents.size() == scheduledEventCache.size() && scheduledEventCache.containsAll(scheduledEvents)) {
536 // Duplicates are not allowed. Reordering is not supported.
537 logger.debug("Preserving scheduled event channels, no changes detected");
538 return scheduledEvents;
541 logger.debug("Updating all scheduled event channels, changes detected");
542 scheduledEventCache = new CopyOnWriteArrayList<ScheduledEvent>(scheduledEvents);
544 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
546 .removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS.equals(c.getUID().getGroupId()));
547 scheduledEvents.stream().forEach(scheduledEvent -> {
548 Channel channel = createScheduledEventChannel(scheduledEvent, scenes, sceneCollections);
549 if (channel != null) {
550 allChannels.add(channel);
553 updateThing(editThing().withChannels(allChannels).build());
555 return scheduledEvents;
558 private @Nullable Channel createScheduledEventChannel(ScheduledEvent scheduledEvent, List<Scene> scenes,
559 List<SceneCollection> sceneCollections) {
560 String referencedName = getReferencedSceneOrSceneCollectionName(scheduledEvent, scenes, sceneCollections);
561 if (referencedName == null) {
564 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
565 HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS);
566 ChannelUID channelUid = new ChannelUID(channelGroupUid, Integer.toString(scheduledEvent.id));
567 String label = getScheduledEventName(referencedName, scheduledEvent);
568 String description = translationProvider.getText("dynamic-channel.automation-enabled.description",
570 Channel channel = ChannelBuilder.create(channelUid, CoreItemFactory.SWITCH).withType(automationChannelTypeUID)
571 .withLabel(label).withDescription(description).build();
576 private @Nullable String getReferencedSceneOrSceneCollectionName(ScheduledEvent scheduledEvent, List<Scene> scenes,
577 List<SceneCollection> sceneCollections) {
578 if (scheduledEvent.sceneId > 0) {
579 for (Scene scene : scenes) {
580 if (scene.id == scheduledEvent.sceneId) {
581 return scene.getName();
584 logger.error("Scene '{}' was not found for scheduled event '{}'", scheduledEvent.sceneId,
587 } else if (scheduledEvent.sceneCollectionId > 0) {
588 for (SceneCollection sceneCollection : sceneCollections) {
589 if (sceneCollection.id == scheduledEvent.sceneCollectionId) {
590 return sceneCollection.getName();
593 logger.error("Scene collection '{}' was not found for scheduled event '{}'",
594 scheduledEvent.sceneCollectionId, scheduledEvent.id);
597 logger.error("Scheduled event '{}'' not related to any scene or scene collection", scheduledEvent.id);
602 private String getScheduledEventName(String sceneName, ScheduledEvent scheduledEvent) {
603 String timeString, daysString;
605 switch (scheduledEvent.eventType) {
606 case ScheduledEvents.SCHEDULED_EVENT_TYPE_TIME:
607 timeString = LocalTime.of(scheduledEvent.hour, scheduledEvent.minute).toString();
609 case ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNRISE:
610 if (scheduledEvent.minute == 0) {
611 timeString = translationProvider.getText("dynamic-channel.automation.at_sunrise");
612 } else if (scheduledEvent.minute < 0) {
613 timeString = translationProvider.getText("dynamic-channel.automation.before_sunrise",
614 getFormattedTimeOffset(-scheduledEvent.minute));
616 timeString = translationProvider.getText("dynamic-channel.automation.after_sunrise",
617 getFormattedTimeOffset(scheduledEvent.minute));
620 case ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNSET:
621 if (scheduledEvent.minute == 0) {
622 timeString = translationProvider.getText("dynamic-channel.automation.at_sunset");
623 } else if (scheduledEvent.minute < 0) {
624 timeString = translationProvider.getText("dynamic-channel.automation.before_sunset",
625 getFormattedTimeOffset(-scheduledEvent.minute));
627 timeString = translationProvider.getText("dynamic-channel.automation.after_sunset",
628 getFormattedTimeOffset(scheduledEvent.minute));
635 EnumSet<DayOfWeek> days = scheduledEvent.getDays();
636 if (EnumSet.allOf(DayOfWeek.class).equals(days)) {
637 daysString = translationProvider.getText("dynamic-channel.automation.all-days");
638 } else if (ScheduledEvents.WEEKDAYS.equals(days)) {
639 daysString = translationProvider.getText("dynamic-channel.automation.weekdays");
640 } else if (ScheduledEvents.WEEKENDS.equals(days)) {
641 daysString = translationProvider.getText("dynamic-channel.automation.weekends");
643 StringJoiner joiner = new StringJoiner(", ");
644 days.forEach(day -> joiner.add(day.getDisplayName(TextStyle.SHORT, translationProvider.getLocale())));
645 daysString = joiner.toString();
648 return translationProvider.getText("dynamic-channel.automation-enabled.label", sceneName, timeString,
652 private String getFormattedTimeOffset(int minutes) {
654 int remainder = minutes % 60;
655 if (remainder == 0) {
656 return translationProvider.getText("dynamic-channel.automation.hour", minutes / 60);
658 return translationProvider.getText("dynamic-channel.automation.hour-minute", minutes / 60, remainder);
660 return translationProvider.getText("dynamic-channel.automation.minute", minutes);
663 private void updateScheduledEventStates(List<ScheduledEvent> scheduledEvents) {
664 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
665 HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS);
666 for (ScheduledEvent scheduledEvent : scheduledEvents) {
667 String scheduledEventId = Integer.toString(scheduledEvent.id);
668 ChannelUID channelUid = new ChannelUID(channelGroupUid, scheduledEventId);
669 updateState(channelUid, scheduledEvent.enabled ? OnOffType.ON : OnOffType.OFF);
673 private Map<Thing, String> getThingIdMap() {
674 Map<Thing, String> ret = new HashMap<>();
675 for (Thing thing : getThing().getThings()) {
676 String id = thing.getConfiguration().as(HDPowerViewShadeConfiguration.class).id;
677 if (id != null && !id.isEmpty()) {
684 private Map<String, ShadeData> getIdShadeDataMap(List<ShadeData> shadeData) {
685 Map<String, ShadeData> ret = new HashMap<>();
686 for (ShadeData shade : shadeData) {
688 ret.put(Integer.toString(shade.id), shade);
694 private void requestRefreshShadePositions() {
695 Map<Thing, String> thingIdMap = getThingIdMap();
696 for (Entry<Thing, String> item : thingIdMap.entrySet()) {
697 Thing thing = item.getKey();
698 ThingHandler handler = thing.getHandler();
699 if (handler instanceof HDPowerViewShadeHandler) {
700 ((HDPowerViewShadeHandler) handler).requestRefreshShadePosition();
702 String shadeId = item.getValue();
703 logger.debug("Shade '{}' handler not initialized", shadeId);
708 private void requestRefreshShadeBatteryLevels() {
709 Map<Thing, String> thingIdMap = getThingIdMap();
710 for (Entry<Thing, String> item : thingIdMap.entrySet()) {
711 Thing thing = item.getKey();
712 ThingHandler handler = thing.getHandler();
713 if (handler instanceof HDPowerViewShadeHandler) {
714 ((HDPowerViewShadeHandler) handler).requestRefreshShadeBatteryLevel();
716 String shadeId = item.getValue();
717 logger.debug("Shade '{}' handler not initialized", shadeId);