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.api.Firmware;
38 import org.openhab.binding.hdpowerview.internal.api.responses.FirmwareVersions;
39 import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections;
40 import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection;
41 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
42 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene;
43 import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents;
44 import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents.ScheduledEvent;
45 import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
46 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
47 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewHubConfiguration;
48 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
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.CoreItemFactory;
54 import org.openhab.core.library.types.OnOffType;
55 import org.openhab.core.thing.Bridge;
56 import org.openhab.core.thing.Channel;
57 import org.openhab.core.thing.ChannelGroupUID;
58 import org.openhab.core.thing.ChannelUID;
59 import org.openhab.core.thing.Thing;
60 import org.openhab.core.thing.ThingStatus;
61 import org.openhab.core.thing.ThingStatusDetail;
62 import org.openhab.core.thing.binding.BaseBridgeHandler;
63 import org.openhab.core.thing.binding.ThingHandler;
64 import org.openhab.core.thing.binding.builder.ChannelBuilder;
65 import org.openhab.core.thing.type.ChannelTypeUID;
66 import org.openhab.core.types.Command;
67 import org.openhab.core.types.RefreshType;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
72 * The {@link HDPowerViewHubHandler} is responsible for handling commands, which
73 * are sent to one of the channels.
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
80 public class HDPowerViewHubHandler extends BaseBridgeHandler {
82 private static final long INITIAL_SOFT_POLL_DELAY_MS = 5_000;
84 private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubHandler.class);
85 private final HttpClient httpClient;
86 private final HDPowerViewTranslationProvider translationProvider;
88 private long refreshInterval;
89 private long hardRefreshPositionInterval;
90 private long hardRefreshBatteryLevelInterval;
92 private @Nullable HDPowerViewWebTargets webTargets;
93 private @Nullable ScheduledFuture<?> pollFuture;
94 private @Nullable ScheduledFuture<?> hardRefreshPositionFuture;
95 private @Nullable ScheduledFuture<?> hardRefreshBatteryLevelFuture;
97 private List<Scene> sceneCache = new CopyOnWriteArrayList<>();
98 private List<SceneCollection> sceneCollectionCache = new CopyOnWriteArrayList<>();
99 private List<ScheduledEvent> scheduledEventCache = new CopyOnWriteArrayList<>();
100 private @Nullable FirmwareVersions firmwareVersions;
101 private Boolean deprecatedChannelsCreated = false;
103 private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
104 HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE);
106 private final ChannelTypeUID sceneGroupChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
107 HDPowerViewBindingConstants.CHANNELTYPE_SCENE_GROUP_ACTIVATE);
109 private final ChannelTypeUID automationChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
110 HDPowerViewBindingConstants.CHANNELTYPE_AUTOMATION_ENABLED);
112 public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient,
113 HDPowerViewTranslationProvider translationProvider) {
115 this.httpClient = httpClient;
116 this.translationProvider = translationProvider;
120 public void handleCommand(ChannelUID channelUID, Command command) {
121 if (RefreshType.REFRESH == command) {
122 requestRefreshShadePositions();
126 Channel channel = getThing().getChannel(channelUID.getId());
127 if (channel == null) {
132 HDPowerViewWebTargets webTargets = this.webTargets;
133 if (webTargets == null) {
134 throw new ProcessingException("Web targets not initialized");
136 int id = Integer.parseInt(channelUID.getIdWithoutGroup());
137 if (sceneChannelTypeUID.equals(channel.getChannelTypeUID()) && OnOffType.ON == command) {
138 webTargets.activateScene(id);
139 // Reschedule soft poll for immediate shade position update.
141 } else if (sceneGroupChannelTypeUID.equals(channel.getChannelTypeUID()) && OnOffType.ON == command) {
142 webTargets.activateSceneCollection(id);
143 // Reschedule soft poll for immediate shade position update.
145 } else if (automationChannelTypeUID.equals(channel.getChannelTypeUID())) {
146 webTargets.enableScheduledEvent(id, OnOffType.ON == command);
148 } catch (HubMaintenanceException e) {
149 // exceptions are logged in HDPowerViewWebTargets
150 } catch (NumberFormatException | HubException e) {
151 logger.debug("Unexpected error {}", e.getMessage());
156 public void initialize() {
157 logger.debug("Initializing hub");
158 HDPowerViewHubConfiguration config = getConfigAs(HDPowerViewHubConfiguration.class);
159 String host = config.host;
161 if (host == null || host.isEmpty()) {
162 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
163 "@text/offline.conf-error.no-host-address");
167 webTargets = new HDPowerViewWebTargets(httpClient, host);
168 refreshInterval = config.refresh;
169 hardRefreshPositionInterval = config.hardRefresh;
170 hardRefreshBatteryLevelInterval = config.hardRefreshBatteryLevel;
171 initializeChannels();
175 private void initializeChannels() {
176 // Rebuild dynamic channels and synchronize with cache.
177 updateThing(editThing().withChannels(new ArrayList<Channel>()).build());
179 sceneCollectionCache.clear();
180 scheduledEventCache.clear();
181 deprecatedChannelsCreated = false;
184 public @Nullable HDPowerViewWebTargets getWebTargets() {
189 public void handleRemoval() {
190 super.handleRemoval();
195 public void dispose() {
200 private void schedulePoll() {
201 scheduleSoftPoll(INITIAL_SOFT_POLL_DELAY_MS);
205 private void scheduleSoftPoll(long initialDelay) {
206 ScheduledFuture<?> future = this.pollFuture;
207 if (future != null) {
208 future.cancel(false);
210 logger.debug("Scheduling poll for {} ms out, then every {} ms", initialDelay, refreshInterval);
211 this.pollFuture = scheduler.scheduleWithFixedDelay(this::poll, initialDelay, refreshInterval,
212 TimeUnit.MILLISECONDS);
215 private void scheduleHardPoll() {
216 ScheduledFuture<?> future = this.hardRefreshPositionFuture;
217 if (future != null) {
218 future.cancel(false);
220 if (hardRefreshPositionInterval > 0) {
221 logger.debug("Scheduling hard position refresh every {} minutes", hardRefreshPositionInterval);
222 this.hardRefreshPositionFuture = scheduler.scheduleWithFixedDelay(this::requestRefreshShadePositions, 1,
223 hardRefreshPositionInterval, TimeUnit.MINUTES);
226 future = this.hardRefreshBatteryLevelFuture;
227 if (future != null) {
228 future.cancel(false);
230 if (hardRefreshBatteryLevelInterval > 0) {
231 logger.debug("Scheduling hard battery level refresh every {} hours", hardRefreshBatteryLevelInterval);
232 this.hardRefreshBatteryLevelFuture = scheduler.scheduleWithFixedDelay(
233 this::requestRefreshShadeBatteryLevels, 1, hardRefreshBatteryLevelInterval, TimeUnit.HOURS);
237 private synchronized void stopPoll() {
238 ScheduledFuture<?> future = this.pollFuture;
239 if (future != null) {
242 this.pollFuture = null;
244 future = this.hardRefreshPositionFuture;
245 if (future != null) {
248 this.hardRefreshPositionFuture = null;
250 future = this.hardRefreshBatteryLevelFuture;
251 if (future != null) {
254 this.hardRefreshBatteryLevelFuture = null;
257 private synchronized void poll() {
259 logger.debug("Polling for state");
260 updateFirmwareProperties();
263 List<Scene> scenes = updateSceneChannels();
264 List<SceneCollection> sceneCollections = updateSceneCollectionChannels();
265 List<ScheduledEvent> scheduledEvents = updateScheduledEventChannels(scenes, sceneCollections);
267 // Scheduled events should also have their current state updated if event has been
268 // enabled or disabled through app or other integration.
269 updateScheduledEventStates(scheduledEvents);
270 } catch (HubInvalidResponseException e) {
271 Throwable cause = e.getCause();
273 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
275 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
277 } catch (HubMaintenanceException e) {
278 // exceptions are logged in HDPowerViewWebTargets
279 } catch (HubException e) {
280 logger.warn("Error connecting to bridge: {}", e.getMessage());
281 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, e.getMessage());
285 private void updateFirmwareProperties()
286 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
287 if (firmwareVersions != null) {
290 HDPowerViewWebTargets webTargets = this.webTargets;
291 if (webTargets == null) {
292 throw new ProcessingException("Web targets not initialized");
294 FirmwareVersions firmwareVersions = webTargets.getFirmwareVersions();
295 Firmware mainProcessor = firmwareVersions.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 String mainProcessorName = mainProcessor.name;
303 if (mainProcessorName != null) {
304 properties.put(HDPowerViewBindingConstants.PROPERTY_FIRMWARE_NAME, mainProcessorName);
306 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, mainProcessor.toString());
307 Firmware radio = firmwareVersions.radio;
309 logger.debug("Radio firmware version received: {}", radio.toString());
310 properties.put(HDPowerViewBindingConstants.PROPERTY_RADIO_FIRMWARE_VERSION, radio.toString());
312 updateProperties(properties);
315 private void pollShades() throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
316 HDPowerViewWebTargets webTargets = this.webTargets;
317 if (webTargets == null) {
318 throw new ProcessingException("Web targets not initialized");
321 Shades shades = webTargets.getShades();
322 List<ShadeData> shadesData = shades.shadeData;
323 if (shadesData == null) {
324 throw new HubInvalidResponseException("Missing 'shades.shadeData' element");
327 updateStatus(ThingStatus.ONLINE);
328 logger.debug("Received data for {} shades", shadesData.size());
330 Map<String, ShadeData> idShadeDataMap = getIdShadeDataMap(shadesData);
331 Map<Thing, String> thingIdMap = getThingIdMap();
332 for (Entry<Thing, String> item : thingIdMap.entrySet()) {
333 Thing thing = item.getKey();
334 String shadeId = item.getValue();
335 ShadeData shadeData = idShadeDataMap.get(shadeId);
336 updateShadeThing(shadeId, thing, shadeData);
340 private void updateShadeThing(String shadeId, Thing thing, @Nullable ShadeData shadeData) {
341 HDPowerViewShadeHandler thingHandler = ((HDPowerViewShadeHandler) thing.getHandler());
342 if (thingHandler == null) {
343 logger.debug("Shade '{}' handler not initialized", shadeId);
346 if (shadeData == null) {
347 logger.debug("Shade '{}' has no data in hub", shadeId);
349 logger.debug("Updating shade '{}'", shadeId);
351 thingHandler.onReceiveUpdate(shadeData);
354 private List<Scene> fetchScenes()
355 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
356 HDPowerViewWebTargets webTargets = this.webTargets;
357 if (webTargets == null) {
358 throw new ProcessingException("Web targets not initialized");
361 Scenes scenes = webTargets.getScenes();
362 List<Scene> sceneData = scenes.sceneData;
363 if (sceneData == null) {
364 throw new HubInvalidResponseException("Missing 'scenes.sceneData' element");
366 logger.debug("Received data for {} scenes", sceneData.size());
371 private List<Scene> updateSceneChannels()
372 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
373 List<Scene> scenes = fetchScenes();
375 if (scenes.size() == sceneCache.size() && sceneCache.containsAll(scenes)) {
376 // Duplicates are not allowed. Reordering is not supported.
377 logger.debug("Preserving scene channels, no changes detected");
381 logger.debug("Updating all scene channels, changes detected");
382 sceneCache = new CopyOnWriteArrayList<Scene>(scenes);
384 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
385 allChannels.removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES.equals(c.getUID().getGroupId()));
386 scenes.stream().sorted().forEach(scene -> allChannels.add(createSceneChannel(scene)));
387 updateThing(editThing().withChannels(allChannels).build());
389 createDeprecatedSceneChannels(scenes);
394 private Channel createSceneChannel(Scene scene) {
395 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
396 HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES);
397 ChannelUID channelUid = new ChannelUID(channelGroupUid, Integer.toString(scene.id));
398 String description = translationProvider.getText("dynamic-channel.scene-activate.description", scene.getName());
399 Channel channel = ChannelBuilder.create(channelUid, CoreItemFactory.SWITCH).withType(sceneChannelTypeUID)
400 .withLabel(scene.getName()).withDescription(description).build();
406 * Create backwards compatible scene channels if any items configured before release 3.2
407 * are still linked. Users should have a reasonable amount of time to migrate to the new
408 * scene channels that are connected to a channel group.
410 private void createDeprecatedSceneChannels(List<Scene> scenes) {
411 if (deprecatedChannelsCreated) {
412 // Only do this once.
415 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
416 HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES);
417 for (Scene scene : scenes) {
418 String channelId = Integer.toString(scene.id);
419 ChannelUID newChannelUid = new ChannelUID(channelGroupUid, channelId);
420 ChannelUID deprecatedChannelUid = new ChannelUID(getThing().getUID(), channelId);
421 String description = translationProvider.getText("dynamic-channel.scene-activate.deprecated.description",
423 Channel channel = ChannelBuilder.create(deprecatedChannelUid, CoreItemFactory.SWITCH)
424 .withType(sceneChannelTypeUID).withLabel(scene.getName()).withDescription(description).build();
425 logger.debug("Creating deprecated channel '{}' ('{}') to probe for linked items", deprecatedChannelUid,
427 updateThing(editThing().withChannel(channel).build());
428 if (this.isLinked(deprecatedChannelUid) && !this.isLinked(newChannelUid)) {
429 logger.warn("Created deprecated channel '{}' ('{}'), please link items to '{}' instead",
430 deprecatedChannelUid, scene.getName(), newChannelUid);
432 if (this.isLinked(newChannelUid)) {
433 logger.debug("Removing deprecated channel '{}' ('{}') since new channel '{}' is linked",
434 deprecatedChannelUid, scene.getName(), newChannelUid);
437 logger.debug("Removing deprecated channel '{}' ('{}') since it has no linked items",
438 deprecatedChannelUid, scene.getName());
440 updateThing(editThing().withoutChannel(deprecatedChannelUid).build());
443 deprecatedChannelsCreated = true;
446 private List<SceneCollection> fetchSceneCollections()
447 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
448 HDPowerViewWebTargets webTargets = this.webTargets;
449 if (webTargets == null) {
450 throw new ProcessingException("Web targets not initialized");
453 SceneCollections sceneCollections = webTargets.getSceneCollections();
454 List<SceneCollection> sceneCollectionData = sceneCollections.sceneCollectionData;
455 if (sceneCollectionData == null) {
456 throw new HubInvalidResponseException("Missing 'sceneCollections.sceneCollectionData' element");
458 logger.debug("Received data for {} sceneCollections", sceneCollectionData.size());
460 return sceneCollectionData;
463 private List<SceneCollection> updateSceneCollectionChannels()
464 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
465 List<SceneCollection> sceneCollections = fetchSceneCollections();
467 if (sceneCollections.size() == sceneCollectionCache.size()
468 && sceneCollectionCache.containsAll(sceneCollections)) {
469 // Duplicates are not allowed. Reordering is not supported.
470 logger.debug("Preserving scene collection channels, no changes detected");
471 return sceneCollections;
474 logger.debug("Updating all scene collection channels, changes detected");
475 sceneCollectionCache = new CopyOnWriteArrayList<SceneCollection>(sceneCollections);
477 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
479 .removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_SCENE_GROUPS.equals(c.getUID().getGroupId()));
480 sceneCollections.stream().sorted()
481 .forEach(sceneCollection -> allChannels.add(createSceneCollectionChannel(sceneCollection)));
482 updateThing(editThing().withChannels(allChannels).build());
484 return sceneCollections;
487 private Channel createSceneCollectionChannel(SceneCollection sceneCollection) {
488 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
489 HDPowerViewBindingConstants.CHANNEL_GROUP_SCENE_GROUPS);
490 ChannelUID channelUid = new ChannelUID(channelGroupUid, Integer.toString(sceneCollection.id));
491 String description = translationProvider.getText("dynamic-channel.scene-group-activate.description",
492 sceneCollection.getName());
493 Channel channel = ChannelBuilder.create(channelUid, CoreItemFactory.SWITCH).withType(sceneGroupChannelTypeUID)
494 .withLabel(sceneCollection.getName()).withDescription(description).build();
499 private List<ScheduledEvent> fetchScheduledEvents()
500 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
501 HDPowerViewWebTargets webTargets = this.webTargets;
502 if (webTargets == null) {
503 throw new ProcessingException("Web targets not initialized");
506 ScheduledEvents scheduledEvents = webTargets.getScheduledEvents();
507 List<ScheduledEvent> scheduledEventData = scheduledEvents.scheduledEventData;
508 if (scheduledEventData == null) {
509 throw new HubInvalidResponseException("Missing 'scheduledEvents.scheduledEventData' element");
511 logger.debug("Received data for {} scheduledEvents", scheduledEventData.size());
513 return scheduledEventData;
516 private List<ScheduledEvent> updateScheduledEventChannels(List<Scene> scenes,
517 List<SceneCollection> sceneCollections)
518 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
519 List<ScheduledEvent> scheduledEvents = fetchScheduledEvents();
521 if (scheduledEvents.size() == scheduledEventCache.size() && scheduledEventCache.containsAll(scheduledEvents)) {
522 // Duplicates are not allowed. Reordering is not supported.
523 logger.debug("Preserving scheduled event channels, no changes detected");
524 return scheduledEvents;
527 logger.debug("Updating all scheduled event channels, changes detected");
528 scheduledEventCache = new CopyOnWriteArrayList<ScheduledEvent>(scheduledEvents);
530 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
532 .removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS.equals(c.getUID().getGroupId()));
533 scheduledEvents.stream().forEach(scheduledEvent -> {
534 Channel channel = createScheduledEventChannel(scheduledEvent, scenes, sceneCollections);
535 if (channel != null) {
536 allChannels.add(channel);
539 updateThing(editThing().withChannels(allChannels).build());
541 return scheduledEvents;
544 private @Nullable Channel createScheduledEventChannel(ScheduledEvent scheduledEvent, List<Scene> scenes,
545 List<SceneCollection> sceneCollections) {
546 String referencedName = getReferencedSceneOrSceneCollectionName(scheduledEvent, scenes, sceneCollections);
547 if (referencedName == null) {
550 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
551 HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS);
552 ChannelUID channelUid = new ChannelUID(channelGroupUid, Integer.toString(scheduledEvent.id));
553 String label = getScheduledEventName(referencedName, scheduledEvent);
554 String description = translationProvider.getText("dynamic-channel.automation-enabled.description",
556 Channel channel = ChannelBuilder.create(channelUid, CoreItemFactory.SWITCH).withType(automationChannelTypeUID)
557 .withLabel(label).withDescription(description).build();
562 private @Nullable String getReferencedSceneOrSceneCollectionName(ScheduledEvent scheduledEvent, List<Scene> scenes,
563 List<SceneCollection> sceneCollections) {
564 if (scheduledEvent.sceneId > 0) {
565 for (Scene scene : scenes) {
566 if (scene.id == scheduledEvent.sceneId) {
567 return scene.getName();
570 logger.error("Scene '{}' was not found for scheduled event '{}'", scheduledEvent.sceneId,
573 } else if (scheduledEvent.sceneCollectionId > 0) {
574 for (SceneCollection sceneCollection : sceneCollections) {
575 if (sceneCollection.id == scheduledEvent.sceneCollectionId) {
576 return sceneCollection.getName();
579 logger.error("Scene collection '{}' was not found for scheduled event '{}'",
580 scheduledEvent.sceneCollectionId, scheduledEvent.id);
583 logger.error("Scheduled event '{}'' not related to any scene or scene collection", scheduledEvent.id);
588 private String getScheduledEventName(String sceneName, ScheduledEvent scheduledEvent) {
589 String timeString, daysString;
591 switch (scheduledEvent.eventType) {
592 case ScheduledEvents.SCHEDULED_EVENT_TYPE_TIME:
593 timeString = LocalTime.of(scheduledEvent.hour, scheduledEvent.minute).toString();
595 case ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNRISE:
596 if (scheduledEvent.minute == 0) {
597 timeString = translationProvider.getText("dynamic-channel.automation.at_sunrise");
598 } else if (scheduledEvent.minute < 0) {
599 timeString = translationProvider.getText("dynamic-channel.automation.before_sunrise",
600 getFormattedTimeOffset(-scheduledEvent.minute));
602 timeString = translationProvider.getText("dynamic-channel.automation.after_sunrise",
603 getFormattedTimeOffset(scheduledEvent.minute));
606 case ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNSET:
607 if (scheduledEvent.minute == 0) {
608 timeString = translationProvider.getText("dynamic-channel.automation.at_sunset");
609 } else if (scheduledEvent.minute < 0) {
610 timeString = translationProvider.getText("dynamic-channel.automation.before_sunset",
611 getFormattedTimeOffset(-scheduledEvent.minute));
613 timeString = translationProvider.getText("dynamic-channel.automation.after_sunset",
614 getFormattedTimeOffset(scheduledEvent.minute));
621 EnumSet<DayOfWeek> days = scheduledEvent.getDays();
622 if (EnumSet.allOf(DayOfWeek.class).equals(days)) {
623 daysString = translationProvider.getText("dynamic-channel.automation.all-days");
624 } else if (ScheduledEvents.WEEKDAYS.equals(days)) {
625 daysString = translationProvider.getText("dynamic-channel.automation.weekdays");
626 } else if (ScheduledEvents.WEEKENDS.equals(days)) {
627 daysString = translationProvider.getText("dynamic-channel.automation.weekends");
629 StringJoiner joiner = new StringJoiner(", ");
630 days.forEach(day -> joiner.add(day.getDisplayName(TextStyle.SHORT, translationProvider.getLocale())));
631 daysString = joiner.toString();
634 return translationProvider.getText("dynamic-channel.automation-enabled.label", sceneName, timeString,
638 private String getFormattedTimeOffset(int minutes) {
640 int remainder = minutes % 60;
641 if (remainder == 0) {
642 return translationProvider.getText("dynamic-channel.automation.hour", minutes / 60);
644 return translationProvider.getText("dynamic-channel.automation.hour-minute", minutes / 60, remainder);
646 return translationProvider.getText("dynamic-channel.automation.minute", minutes);
649 private void updateScheduledEventStates(List<ScheduledEvent> scheduledEvents) {
650 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
651 HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS);
652 for (ScheduledEvent scheduledEvent : scheduledEvents) {
653 String scheduledEventId = Integer.toString(scheduledEvent.id);
654 ChannelUID channelUid = new ChannelUID(channelGroupUid, scheduledEventId);
655 updateState(channelUid, scheduledEvent.enabled ? OnOffType.ON : OnOffType.OFF);
659 private Map<Thing, String> getThingIdMap() {
660 Map<Thing, String> ret = new HashMap<>();
661 for (Thing thing : getThing().getThings()) {
662 String id = thing.getConfiguration().as(HDPowerViewShadeConfiguration.class).id;
663 if (id != null && !id.isEmpty()) {
670 private Map<String, ShadeData> getIdShadeDataMap(List<ShadeData> shadeData) {
671 Map<String, ShadeData> ret = new HashMap<>();
672 for (ShadeData shade : shadeData) {
674 ret.put(Integer.toString(shade.id), shade);
680 private void requestRefreshShadePositions() {
681 Map<Thing, String> thingIdMap = getThingIdMap();
682 for (Entry<Thing, String> item : thingIdMap.entrySet()) {
683 Thing thing = item.getKey();
684 ThingHandler handler = thing.getHandler();
685 if (handler instanceof HDPowerViewShadeHandler) {
686 ((HDPowerViewShadeHandler) handler).requestRefreshShadePosition();
688 String shadeId = item.getValue();
689 logger.debug("Shade '{}' handler not initialized", shadeId);
694 private void requestRefreshShadeBatteryLevels() {
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).requestRefreshShadeBatteryLevel();
702 String shadeId = item.getValue();
703 logger.debug("Shade '{}' handler not initialized", shadeId);