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.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.core.library.CoreItemFactory;
50 import org.openhab.core.library.types.OnOffType;
51 import org.openhab.core.thing.Bridge;
52 import org.openhab.core.thing.Channel;
53 import org.openhab.core.thing.ChannelGroupUID;
54 import org.openhab.core.thing.ChannelUID;
55 import org.openhab.core.thing.Thing;
56 import org.openhab.core.thing.ThingStatus;
57 import org.openhab.core.thing.ThingStatusDetail;
58 import org.openhab.core.thing.binding.BaseBridgeHandler;
59 import org.openhab.core.thing.binding.ThingHandler;
60 import org.openhab.core.thing.binding.builder.ChannelBuilder;
61 import org.openhab.core.thing.type.ChannelTypeUID;
62 import org.openhab.core.types.Command;
63 import org.openhab.core.types.RefreshType;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
67 import com.google.gson.JsonParseException;
70 * The {@link HDPowerViewHubHandler} is responsible for handling commands, which
71 * are sent to one of the channels.
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
78 public class HDPowerViewHubHandler extends BaseBridgeHandler {
80 private static final long INITIAL_SOFT_POLL_DELAY_MS = 5_000;
82 private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubHandler.class);
83 private final HttpClient httpClient;
84 private final HDPowerViewTranslationProvider translationProvider;
86 private long refreshInterval;
87 private long hardRefreshPositionInterval;
88 private long hardRefreshBatteryLevelInterval;
90 private @Nullable HDPowerViewWebTargets webTargets;
91 private @Nullable ScheduledFuture<?> pollFuture;
92 private @Nullable ScheduledFuture<?> hardRefreshPositionFuture;
93 private @Nullable ScheduledFuture<?> hardRefreshBatteryLevelFuture;
95 private List<Scene> sceneCache = new CopyOnWriteArrayList<>();
96 private List<SceneCollection> sceneCollectionCache = new CopyOnWriteArrayList<>();
97 private List<ScheduledEvent> scheduledEventCache = new CopyOnWriteArrayList<>();
98 private Boolean deprecatedChannelsCreated = false;
100 private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
101 HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE);
103 private final ChannelTypeUID sceneGroupChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
104 HDPowerViewBindingConstants.CHANNELTYPE_SCENE_GROUP_ACTIVATE);
106 private final ChannelTypeUID automationChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
107 HDPowerViewBindingConstants.CHANNELTYPE_AUTOMATION_ENABLED);
109 public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient,
110 HDPowerViewTranslationProvider translationProvider) {
112 this.httpClient = httpClient;
113 this.translationProvider = translationProvider;
117 public void handleCommand(ChannelUID channelUID, Command command) {
118 if (RefreshType.REFRESH == command) {
119 requestRefreshShadePositions();
123 Channel channel = getThing().getChannel(channelUID.getId());
124 if (channel == null) {
129 HDPowerViewWebTargets webTargets = this.webTargets;
130 if (webTargets == null) {
131 throw new ProcessingException("Web targets not initialized");
133 int id = Integer.parseInt(channelUID.getIdWithoutGroup());
134 if (sceneChannelTypeUID.equals(channel.getChannelTypeUID()) && OnOffType.ON == command) {
135 webTargets.activateScene(id);
136 // Reschedule soft poll for immediate shade position update.
138 } else if (sceneGroupChannelTypeUID.equals(channel.getChannelTypeUID()) && OnOffType.ON == command) {
139 webTargets.activateSceneCollection(id);
140 // Reschedule soft poll for immediate shade position update.
142 } else if (automationChannelTypeUID.equals(channel.getChannelTypeUID())) {
143 webTargets.enableScheduledEvent(id, OnOffType.ON == command);
145 } catch (HubMaintenanceException e) {
146 // exceptions are logged in HDPowerViewWebTargets
147 } catch (NumberFormatException | HubProcessingException e) {
148 logger.debug("Unexpected error {}", e.getMessage());
153 public void initialize() {
154 logger.debug("Initializing hub");
155 HDPowerViewHubConfiguration config = getConfigAs(HDPowerViewHubConfiguration.class);
156 String host = config.host;
158 if (host == null || host.isEmpty()) {
159 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
160 "@text/offline.conf-error.no-host-address");
164 webTargets = new HDPowerViewWebTargets(httpClient, host);
165 refreshInterval = config.refresh;
166 hardRefreshPositionInterval = config.hardRefresh;
167 hardRefreshBatteryLevelInterval = config.hardRefreshBatteryLevel;
168 initializeChannels();
172 private void initializeChannels() {
173 // Rebuild dynamic channels and synchronize with cache.
174 updateThing(editThing().withChannels(new ArrayList<Channel>()).build());
176 sceneCollectionCache.clear();
177 scheduledEventCache.clear();
178 deprecatedChannelsCreated = false;
181 public @Nullable HDPowerViewWebTargets getWebTargets() {
186 public void handleRemoval() {
187 super.handleRemoval();
192 public void dispose() {
197 private void schedulePoll() {
198 scheduleSoftPoll(INITIAL_SOFT_POLL_DELAY_MS);
202 private void scheduleSoftPoll(long initialDelay) {
203 ScheduledFuture<?> future = this.pollFuture;
204 if (future != null) {
205 future.cancel(false);
207 logger.debug("Scheduling poll for {} ms out, then every {} ms", initialDelay, refreshInterval);
208 this.pollFuture = scheduler.scheduleWithFixedDelay(this::poll, initialDelay, refreshInterval,
209 TimeUnit.MILLISECONDS);
212 private void scheduleHardPoll() {
213 ScheduledFuture<?> future = this.hardRefreshPositionFuture;
214 if (future != null) {
215 future.cancel(false);
217 if (hardRefreshPositionInterval > 0) {
218 logger.debug("Scheduling hard position refresh every {} minutes", hardRefreshPositionInterval);
219 this.hardRefreshPositionFuture = scheduler.scheduleWithFixedDelay(this::requestRefreshShadePositions, 1,
220 hardRefreshPositionInterval, TimeUnit.MINUTES);
223 future = this.hardRefreshBatteryLevelFuture;
224 if (future != null) {
225 future.cancel(false);
227 if (hardRefreshBatteryLevelInterval > 0) {
228 logger.debug("Scheduling hard battery level refresh every {} hours", hardRefreshBatteryLevelInterval);
229 this.hardRefreshBatteryLevelFuture = scheduler.scheduleWithFixedDelay(
230 this::requestRefreshShadeBatteryLevels, 1, hardRefreshBatteryLevelInterval, TimeUnit.HOURS);
234 private synchronized void stopPoll() {
235 ScheduledFuture<?> future = this.pollFuture;
236 if (future != null) {
239 this.pollFuture = null;
241 future = this.hardRefreshPositionFuture;
242 if (future != null) {
245 this.hardRefreshPositionFuture = null;
247 future = this.hardRefreshBatteryLevelFuture;
248 if (future != null) {
251 this.hardRefreshBatteryLevelFuture = null;
254 private synchronized void poll() {
256 logger.debug("Polling for state");
259 List<Scene> scenes = updateSceneChannels();
260 List<SceneCollection> sceneCollections = updateSceneCollectionChannels();
261 List<ScheduledEvent> scheduledEvents = updateScheduledEventChannels(scenes, sceneCollections);
263 // Scheduled events should also have their current state updated if event has been
264 // enabled or disabled through app or other integration.
265 updateScheduledEventStates(scheduledEvents);
266 } catch (JsonParseException e) {
267 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
268 } catch (HubProcessingException e) {
269 logger.warn("Error connecting to bridge: {}", e.getMessage());
270 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, e.getMessage());
271 } catch (HubMaintenanceException e) {
272 // exceptions are logged in HDPowerViewWebTargets
276 private void pollShades() throws JsonParseException, HubProcessingException, HubMaintenanceException {
277 HDPowerViewWebTargets webTargets = this.webTargets;
278 if (webTargets == null) {
279 throw new ProcessingException("Web targets not initialized");
282 Shades shades = webTargets.getShades();
283 if (shades == null) {
284 throw new JsonParseException("Missing 'shades' element");
287 List<ShadeData> shadesData = shades.shadeData;
288 if (shadesData == null) {
289 throw new JsonParseException("Missing 'shades.shadeData' element");
292 updateStatus(ThingStatus.ONLINE);
293 logger.debug("Received data for {} shades", shadesData.size());
295 Map<String, ShadeData> idShadeDataMap = getIdShadeDataMap(shadesData);
296 Map<Thing, String> thingIdMap = getThingIdMap();
297 for (Entry<Thing, String> item : thingIdMap.entrySet()) {
298 Thing thing = item.getKey();
299 String shadeId = item.getValue();
300 ShadeData shadeData = idShadeDataMap.get(shadeId);
301 updateShadeThing(shadeId, thing, shadeData);
305 private void updateShadeThing(String shadeId, Thing thing, @Nullable ShadeData shadeData) {
306 HDPowerViewShadeHandler thingHandler = ((HDPowerViewShadeHandler) thing.getHandler());
307 if (thingHandler == null) {
308 logger.debug("Shade '{}' handler not initialized", shadeId);
311 if (shadeData == null) {
312 logger.debug("Shade '{}' has no data in hub", shadeId);
314 logger.debug("Updating shade '{}'", shadeId);
316 thingHandler.onReceiveUpdate(shadeData);
319 private List<Scene> fetchScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException {
320 HDPowerViewWebTargets webTargets = this.webTargets;
321 if (webTargets == null) {
322 throw new ProcessingException("Web targets not initialized");
325 Scenes scenes = webTargets.getScenes();
326 if (scenes == null) {
327 throw new JsonParseException("Missing 'scenes' element");
330 List<Scene> sceneData = scenes.sceneData;
331 if (sceneData == null) {
332 throw new JsonParseException("Missing 'scenes.sceneData' element");
334 logger.debug("Received data for {} scenes", sceneData.size());
339 private List<Scene> updateSceneChannels()
340 throws JsonParseException, HubProcessingException, HubMaintenanceException {
341 List<Scene> scenes = fetchScenes();
343 if (scenes.size() == sceneCache.size() && sceneCache.containsAll(scenes)) {
344 // Duplicates are not allowed. Reordering is not supported.
345 logger.debug("Preserving scene channels, no changes detected");
349 logger.debug("Updating all scene channels, changes detected");
350 sceneCache = new CopyOnWriteArrayList<Scene>(scenes);
352 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
353 allChannels.removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES.equals(c.getUID().getGroupId()));
354 scenes.stream().sorted().forEach(scene -> allChannels.add(createSceneChannel(scene)));
355 updateThing(editThing().withChannels(allChannels).build());
357 createDeprecatedSceneChannels(scenes);
362 private Channel createSceneChannel(Scene scene) {
363 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
364 HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES);
365 ChannelUID channelUid = new ChannelUID(channelGroupUid, Integer.toString(scene.id));
366 String description = translationProvider.getText("dynamic-channel.scene-activate.description", scene.getName());
367 Channel channel = ChannelBuilder.create(channelUid, CoreItemFactory.SWITCH).withType(sceneChannelTypeUID)
368 .withLabel(scene.getName()).withDescription(description).build();
374 * Create backwards compatible scene channels if any items configured before release 3.2
375 * are still linked. Users should have a reasonable amount of time to migrate to the new
376 * scene channels that are connected to a channel group.
378 private void createDeprecatedSceneChannels(List<Scene> scenes) {
379 if (deprecatedChannelsCreated) {
380 // Only do this once.
383 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
384 HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES);
385 for (Scene scene : scenes) {
386 String channelId = Integer.toString(scene.id);
387 ChannelUID newChannelUid = new ChannelUID(channelGroupUid, channelId);
388 ChannelUID deprecatedChannelUid = new ChannelUID(getThing().getUID(), channelId);
389 String description = translationProvider.getText("dynamic-channel.scene-activate.deprecated.description",
391 Channel channel = ChannelBuilder.create(deprecatedChannelUid, CoreItemFactory.SWITCH)
392 .withType(sceneChannelTypeUID).withLabel(scene.getName()).withDescription(description).build();
393 logger.debug("Creating deprecated channel '{}' ('{}') to probe for linked items", deprecatedChannelUid,
395 updateThing(editThing().withChannel(channel).build());
396 if (this.isLinked(deprecatedChannelUid) && !this.isLinked(newChannelUid)) {
397 logger.warn("Created deprecated channel '{}' ('{}'), please link items to '{}' instead",
398 deprecatedChannelUid, scene.getName(), newChannelUid);
400 if (this.isLinked(newChannelUid)) {
401 logger.debug("Removing deprecated channel '{}' ('{}') since new channel '{}' is linked",
402 deprecatedChannelUid, scene.getName(), newChannelUid);
405 logger.debug("Removing deprecated channel '{}' ('{}') since it has no linked items",
406 deprecatedChannelUid, scene.getName());
408 updateThing(editThing().withoutChannel(deprecatedChannelUid).build());
411 deprecatedChannelsCreated = true;
414 private List<SceneCollection> fetchSceneCollections()
415 throws JsonParseException, HubProcessingException, HubMaintenanceException {
416 HDPowerViewWebTargets webTargets = this.webTargets;
417 if (webTargets == null) {
418 throw new ProcessingException("Web targets not initialized");
421 SceneCollections sceneCollections = webTargets.getSceneCollections();
422 if (sceneCollections == null) {
423 throw new JsonParseException("Missing 'sceneCollections' element");
426 List<SceneCollection> sceneCollectionData = sceneCollections.sceneCollectionData;
427 if (sceneCollectionData == null) {
428 throw new JsonParseException("Missing 'sceneCollections.sceneCollectionData' element");
430 logger.debug("Received data for {} sceneCollections", sceneCollectionData.size());
432 return sceneCollectionData;
435 private List<SceneCollection> updateSceneCollectionChannels()
436 throws JsonParseException, HubProcessingException, HubMaintenanceException {
437 List<SceneCollection> sceneCollections = fetchSceneCollections();
439 if (sceneCollections.size() == sceneCollectionCache.size()
440 && sceneCollectionCache.containsAll(sceneCollections)) {
441 // Duplicates are not allowed. Reordering is not supported.
442 logger.debug("Preserving scene collection channels, no changes detected");
443 return sceneCollections;
446 logger.debug("Updating all scene collection channels, changes detected");
447 sceneCollectionCache = new CopyOnWriteArrayList<SceneCollection>(sceneCollections);
449 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
451 .removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_SCENE_GROUPS.equals(c.getUID().getGroupId()));
452 sceneCollections.stream().sorted()
453 .forEach(sceneCollection -> allChannels.add(createSceneCollectionChannel(sceneCollection)));
454 updateThing(editThing().withChannels(allChannels).build());
456 return sceneCollections;
459 private Channel createSceneCollectionChannel(SceneCollection sceneCollection) {
460 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
461 HDPowerViewBindingConstants.CHANNEL_GROUP_SCENE_GROUPS);
462 ChannelUID channelUid = new ChannelUID(channelGroupUid, Integer.toString(sceneCollection.id));
463 String description = translationProvider.getText("dynamic-channel.scene-group-activate.description",
464 sceneCollection.getName());
465 Channel channel = ChannelBuilder.create(channelUid, CoreItemFactory.SWITCH).withType(sceneGroupChannelTypeUID)
466 .withLabel(sceneCollection.getName()).withDescription(description).build();
471 private List<ScheduledEvent> fetchScheduledEvents()
472 throws JsonParseException, HubProcessingException, HubMaintenanceException {
473 HDPowerViewWebTargets webTargets = this.webTargets;
474 if (webTargets == null) {
475 throw new ProcessingException("Web targets not initialized");
478 ScheduledEvents scheduledEvents = webTargets.getScheduledEvents();
479 if (scheduledEvents == null) {
480 throw new JsonParseException("Missing 'scheduledEvents' element");
483 List<ScheduledEvent> scheduledEventData = scheduledEvents.scheduledEventData;
484 if (scheduledEventData == null) {
485 throw new JsonParseException("Missing 'scheduledEvents.scheduledEventData' element");
487 logger.debug("Received data for {} scheduledEvents", scheduledEventData.size());
489 return scheduledEventData;
492 private List<ScheduledEvent> updateScheduledEventChannels(List<Scene> scenes,
493 List<SceneCollection> sceneCollections)
494 throws JsonParseException, HubProcessingException, HubMaintenanceException {
495 List<ScheduledEvent> scheduledEvents = fetchScheduledEvents();
497 if (scheduledEvents.size() == scheduledEventCache.size() && scheduledEventCache.containsAll(scheduledEvents)) {
498 // Duplicates are not allowed. Reordering is not supported.
499 logger.debug("Preserving scheduled event channels, no changes detected");
500 return scheduledEvents;
503 logger.debug("Updating all scheduled event channels, changes detected");
504 scheduledEventCache = new CopyOnWriteArrayList<ScheduledEvent>(scheduledEvents);
506 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
508 .removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS.equals(c.getUID().getGroupId()));
509 scheduledEvents.stream().forEach(scheduledEvent -> {
510 Channel channel = createScheduledEventChannel(scheduledEvent, scenes, sceneCollections);
511 if (channel != null) {
512 allChannels.add(channel);
515 updateThing(editThing().withChannels(allChannels).build());
517 return scheduledEvents;
520 private @Nullable Channel createScheduledEventChannel(ScheduledEvent scheduledEvent, List<Scene> scenes,
521 List<SceneCollection> sceneCollections) {
522 String referencedName = getReferencedSceneOrSceneCollectionName(scheduledEvent, scenes, sceneCollections);
523 if (referencedName == null) {
526 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
527 HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS);
528 ChannelUID channelUid = new ChannelUID(channelGroupUid, Integer.toString(scheduledEvent.id));
529 String label = getScheduledEventName(referencedName, scheduledEvent);
530 String description = translationProvider.getText("dynamic-channel.automation-enabled.description",
532 Channel channel = ChannelBuilder.create(channelUid, CoreItemFactory.SWITCH).withType(automationChannelTypeUID)
533 .withLabel(label).withDescription(description).build();
538 private @Nullable String getReferencedSceneOrSceneCollectionName(ScheduledEvent scheduledEvent, List<Scene> scenes,
539 List<SceneCollection> sceneCollections) {
540 if (scheduledEvent.sceneId > 0) {
541 for (Scene scene : scenes) {
542 if (scene.id == scheduledEvent.sceneId) {
543 return scene.getName();
546 logger.error("Scene '{}' was not found for scheduled event '{}'", scheduledEvent.sceneId,
549 } else if (scheduledEvent.sceneCollectionId > 0) {
550 for (SceneCollection sceneCollection : sceneCollections) {
551 if (sceneCollection.id == scheduledEvent.sceneCollectionId) {
552 return sceneCollection.getName();
555 logger.error("Scene collection '{}' was not found for scheduled event '{}'",
556 scheduledEvent.sceneCollectionId, scheduledEvent.id);
559 logger.error("Scheduled event '{}'' not related to any scene or scene collection", scheduledEvent.id);
564 private String getScheduledEventName(String sceneName, ScheduledEvent scheduledEvent) {
565 String timeString, daysString;
567 switch (scheduledEvent.eventType) {
568 case ScheduledEvents.SCHEDULED_EVENT_TYPE_TIME:
569 timeString = LocalTime.of(scheduledEvent.hour, scheduledEvent.minute).toString();
571 case ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNRISE:
572 if (scheduledEvent.minute == 0) {
573 timeString = translationProvider.getText("dynamic-channel.automation.at_sunrise");
574 } else if (scheduledEvent.minute < 0) {
575 timeString = translationProvider.getText("dynamic-channel.automation.before_sunrise",
576 getFormattedTimeOffset(-scheduledEvent.minute));
578 timeString = translationProvider.getText("dynamic-channel.automation.after_sunrise",
579 getFormattedTimeOffset(scheduledEvent.minute));
582 case ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNSET:
583 if (scheduledEvent.minute == 0) {
584 timeString = translationProvider.getText("dynamic-channel.automation.at_sunset");
585 } else if (scheduledEvent.minute < 0) {
586 timeString = translationProvider.getText("dynamic-channel.automation.before_sunset",
587 getFormattedTimeOffset(-scheduledEvent.minute));
589 timeString = translationProvider.getText("dynamic-channel.automation.after_sunset",
590 getFormattedTimeOffset(scheduledEvent.minute));
597 EnumSet<DayOfWeek> days = scheduledEvent.getDays();
598 if (EnumSet.allOf(DayOfWeek.class).equals(days)) {
599 daysString = translationProvider.getText("dynamic-channel.automation.all-days");
600 } else if (ScheduledEvents.WEEKDAYS.equals(days)) {
601 daysString = translationProvider.getText("dynamic-channel.automation.weekdays");
602 } else if (ScheduledEvents.WEEKENDS.equals(days)) {
603 daysString = translationProvider.getText("dynamic-channel.automation.weekends");
605 StringJoiner joiner = new StringJoiner(", ");
606 days.forEach(day -> joiner.add(day.getDisplayName(TextStyle.SHORT, translationProvider.getLocale())));
607 daysString = joiner.toString();
610 return translationProvider.getText("dynamic-channel.automation-enabled.label", sceneName, timeString,
614 private String getFormattedTimeOffset(int minutes) {
616 int remainder = minutes % 60;
617 if (remainder == 0) {
618 return translationProvider.getText("dynamic-channel.automation.hour", minutes / 60);
620 return translationProvider.getText("dynamic-channel.automation.hour-minute", minutes / 60, remainder);
622 return translationProvider.getText("dynamic-channel.automation.minute", minutes);
625 private void updateScheduledEventStates(List<ScheduledEvent> scheduledEvents) {
626 ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
627 HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS);
628 for (ScheduledEvent scheduledEvent : scheduledEvents) {
629 String scheduledEventId = Integer.toString(scheduledEvent.id);
630 ChannelUID channelUid = new ChannelUID(channelGroupUid, scheduledEventId);
631 updateState(channelUid, scheduledEvent.enabled ? OnOffType.ON : OnOffType.OFF);
635 private Map<Thing, String> getThingIdMap() {
636 Map<Thing, String> ret = new HashMap<>();
637 for (Thing thing : getThing().getThings()) {
638 String id = thing.getConfiguration().as(HDPowerViewShadeConfiguration.class).id;
639 if (id != null && !id.isEmpty()) {
646 private Map<String, ShadeData> getIdShadeDataMap(List<ShadeData> shadeData) {
647 Map<String, ShadeData> ret = new HashMap<>();
648 for (ShadeData shade : shadeData) {
650 ret.put(Integer.toString(shade.id), shade);
656 private void requestRefreshShadePositions() {
657 Map<Thing, String> thingIdMap = getThingIdMap();
658 for (Entry<Thing, String> item : thingIdMap.entrySet()) {
659 Thing thing = item.getKey();
660 ThingHandler handler = thing.getHandler();
661 if (handler instanceof HDPowerViewShadeHandler) {
662 ((HDPowerViewShadeHandler) handler).requestRefreshShadePosition();
664 String shadeId = item.getValue();
665 logger.debug("Shade '{}' handler not initialized", shadeId);
670 private void requestRefreshShadeBatteryLevels() {
671 Map<Thing, String> thingIdMap = getThingIdMap();
672 for (Entry<Thing, String> item : thingIdMap.entrySet()) {
673 Thing thing = item.getKey();
674 ThingHandler handler = thing.getHandler();
675 if (handler instanceof HDPowerViewShadeHandler) {
676 ((HDPowerViewShadeHandler) handler).requestRefreshShadeBatteryLevel();
678 String shadeId = item.getValue();
679 logger.debug("Shade '{}' handler not initialized", shadeId);