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.util.ArrayList;
16 import java.util.HashMap;
17 import java.util.List;
19 import java.util.Map.Entry;
20 import java.util.concurrent.CopyOnWriteArrayList;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import javax.ws.rs.ProcessingException;
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;
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 @Nullable FirmwareVersions firmwareVersions;
99 private Boolean deprecatedChannelsCreated = false;
101 private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
102 HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE);
104 private final ChannelTypeUID sceneGroupChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
105 HDPowerViewBindingConstants.CHANNELTYPE_SCENE_GROUP_ACTIVATE);
107 private final ChannelTypeUID automationChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
108 HDPowerViewBindingConstants.CHANNELTYPE_AUTOMATION_ENABLED);
110 public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient,
111 HDPowerViewTranslationProvider translationProvider) {
113 this.httpClient = httpClient;
114 this.translationProvider = translationProvider;
118 public void handleCommand(ChannelUID channelUID, Command command) {
119 if (RefreshType.REFRESH == command) {
120 requestRefreshShadePositions();
124 Channel channel = getThing().getChannel(channelUID.getId());
125 if (channel == null) {
130 HDPowerViewWebTargets webTargets = this.webTargets;
131 if (webTargets == null) {
132 throw new ProcessingException("Web targets not initialized");
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.
139 } else if (sceneGroupChannelTypeUID.equals(channel.getChannelTypeUID()) && OnOffType.ON == command) {
140 webTargets.activateSceneCollection(id);
141 // Reschedule soft poll for immediate shade position update.
143 } else if (automationChannelTypeUID.equals(channel.getChannelTypeUID())) {
144 webTargets.enableScheduledEvent(id, OnOffType.ON == command);
146 } catch (HubMaintenanceException e) {
147 // exceptions are logged in HDPowerViewWebTargets
148 } catch (NumberFormatException | HubException e) {
149 logger.debug("Unexpected error {}", e.getMessage());
154 public void initialize() {
155 logger.debug("Initializing hub");
156 HDPowerViewHubConfiguration config = getConfigAs(HDPowerViewHubConfiguration.class);
157 String host = config.host;
159 if (host == null || host.isEmpty()) {
160 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
161 "@text/offline.conf-error.no-host-address");
165 webTargets = new HDPowerViewWebTargets(httpClient, host);
166 refreshInterval = config.refresh;
167 hardRefreshPositionInterval = config.hardRefresh;
168 hardRefreshBatteryLevelInterval = config.hardRefreshBatteryLevel;
169 initializeChannels();
173 private void initializeChannels() {
174 // Rebuild dynamic channels and synchronize with cache.
175 updateThing(editThing().withChannels(new ArrayList<Channel>()).build());
177 sceneCollectionCache.clear();
178 scheduledEventCache.clear();
179 deprecatedChannelsCreated = false;
182 public @Nullable HDPowerViewWebTargets getWebTargets() {
187 public void handleRemoval() {
188 super.handleRemoval();
193 public void dispose() {
198 private void schedulePoll() {
199 scheduleSoftPoll(INITIAL_SOFT_POLL_DELAY_MS);
203 private void scheduleSoftPoll(long initialDelay) {
204 ScheduledFuture<?> future = this.pollFuture;
205 if (future != null) {
206 future.cancel(false);
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);
213 private void scheduleHardPoll() {
214 ScheduledFuture<?> future = this.hardRefreshPositionFuture;
215 if (future != null) {
216 future.cancel(false);
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);
224 future = this.hardRefreshBatteryLevelFuture;
225 if (future != null) {
226 future.cancel(false);
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);
235 private synchronized void stopPoll() {
236 ScheduledFuture<?> future = this.pollFuture;
237 if (future != null) {
240 this.pollFuture = null;
242 future = this.hardRefreshPositionFuture;
243 if (future != null) {
246 this.hardRefreshPositionFuture = null;
248 future = this.hardRefreshBatteryLevelFuture;
249 if (future != null) {
252 this.hardRefreshBatteryLevelFuture = null;
255 private synchronized void poll() {
257 logger.debug("Polling for state");
258 updateFirmwareProperties();
261 List<Scene> scenes = updateSceneChannels();
262 List<SceneCollection> sceneCollections = updateSceneGroupChannels();
263 List<ScheduledEvent> scheduledEvents = updateAutomationChannels(scenes, sceneCollections);
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();
271 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
273 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
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());
283 private void updateFirmwareProperties()
284 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
285 if (firmwareVersions != null) {
288 HDPowerViewWebTargets webTargets = this.webTargets;
289 if (webTargets == null) {
290 throw new ProcessingException("Web targets not initialized");
292 FirmwareVersions firmwareVersions = webTargets.getFirmwareVersions();
293 Firmware mainProcessor = firmwareVersions.mainProcessor;
294 if (mainProcessor == null) {
295 logger.warn("Main processor firmware version missing in response.");
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);
304 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, mainProcessor.toString());
305 Firmware radio = firmwareVersions.radio;
307 logger.debug("Radio firmware version received: {}", radio.toString());
308 properties.put(HDPowerViewBindingConstants.PROPERTY_RADIO_FIRMWARE_VERSION, radio.toString());
310 updateProperties(properties);
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");
319 Shades shades = webTargets.getShades();
320 List<ShadeData> shadesData = shades.shadeData;
321 if (shadesData == null) {
322 throw new HubInvalidResponseException("Missing 'shades.shadeData' element");
325 updateStatus(ThingStatus.ONLINE);
326 logger.debug("Received data for {} shades", shadesData.size());
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);
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);
344 if (shadeData == null) {
345 logger.debug("Shade '{}' has no data in hub", shadeId);
347 logger.debug("Updating shade '{}'", shadeId);
349 thingHandler.onReceiveUpdate(shadeData);
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");
359 Scenes scenes = webTargets.getScenes();
360 List<Scene> sceneData = scenes.sceneData;
361 if (sceneData == null) {
362 throw new HubInvalidResponseException("Missing 'scenes.sceneData' element");
364 logger.debug("Received data for {} scenes", sceneData.size());
369 private List<Scene> updateSceneChannels()
370 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
371 List<Scene> scenes = fetchScenes();
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");
379 logger.debug("Updating all scene channels, changes detected");
380 sceneCache = new CopyOnWriteArrayList<Scene>(scenes);
382 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
383 allChannels.removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES.equals(c.getUID().getGroupId()));
385 SceneChannelBuilder channelBuilder = SceneChannelBuilder
386 .create(this.translationProvider,
387 new ChannelGroupUID(thing.getUID(), HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES))
388 .withScenes(scenes).withChannels(allChannels);
390 updateThing(editThing().withChannels(channelBuilder.build()).build());
392 createDeprecatedSceneChannels(scenes);
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.
402 private void createDeprecatedSceneChannels(List<Scene> scenes) {
403 if (deprecatedChannelsCreated) {
404 // Only do this once.
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",
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,
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);
424 if (this.isLinked(newChannelUid)) {
425 logger.debug("Removing deprecated channel '{}' ('{}') since new channel '{}' is linked",
426 deprecatedChannelUid, scene.getName(), newChannelUid);
429 logger.debug("Removing deprecated channel '{}' ('{}') since it has no linked items",
430 deprecatedChannelUid, scene.getName());
432 updateThing(editThing().withoutChannel(deprecatedChannelUid).build());
435 deprecatedChannelsCreated = true;
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");
445 SceneCollections sceneCollections = webTargets.getSceneCollections();
446 List<SceneCollection> sceneCollectionData = sceneCollections.sceneCollectionData;
447 if (sceneCollectionData == null) {
448 throw new HubInvalidResponseException("Missing 'sceneCollections.sceneCollectionData' element");
450 logger.debug("Received data for {} sceneCollections", sceneCollectionData.size());
452 return sceneCollectionData;
455 private List<SceneCollection> updateSceneGroupChannels()
456 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
457 List<SceneCollection> sceneCollections = fetchSceneCollections();
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;
466 logger.debug("Updating all scene group channels, changes detected");
467 sceneCollectionCache = new CopyOnWriteArrayList<SceneCollection>(sceneCollections);
469 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
471 .removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_SCENE_GROUPS.equals(c.getUID().getGroupId()));
473 SceneGroupChannelBuilder channelBuilder = SceneGroupChannelBuilder
474 .create(this.translationProvider,
475 new ChannelGroupUID(thing.getUID(), HDPowerViewBindingConstants.CHANNEL_GROUP_SCENE_GROUPS))
476 .withSceneCollections(sceneCollections).withChannels(allChannels);
478 updateThing(editThing().withChannels(channelBuilder.build()).build());
480 return sceneCollections;
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");
490 ScheduledEvents scheduledEvents = webTargets.getScheduledEvents();
491 List<ScheduledEvent> scheduledEventData = scheduledEvents.scheduledEventData;
492 if (scheduledEventData == null) {
493 throw new HubInvalidResponseException("Missing 'scheduledEvents.scheduledEventData' element");
495 logger.debug("Received data for {} scheduledEvents", scheduledEventData.size());
497 return scheduledEventData;
500 private List<ScheduledEvent> updateAutomationChannels(List<Scene> scenes, List<SceneCollection> sceneCollections)
501 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
502 List<ScheduledEvent> scheduledEvents = fetchScheduledEvents();
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;
510 logger.debug("Updating all automation channels, changes detected");
511 scheduledEventCache = new CopyOnWriteArrayList<ScheduledEvent>(scheduledEvents);
513 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
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());
523 return scheduledEvents;
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);
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()) {
547 private Map<String, ShadeData> getIdShadeDataMap(List<ShadeData> shadeData) {
548 Map<String, ShadeData> ret = new HashMap<>();
549 for (ShadeData shade : shadeData) {
551 ret.put(Integer.toString(shade.id), shade);
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();
565 String shadeId = item.getValue();
566 logger.debug("Shade '{}' handler not initialized", shadeId);
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();
579 String shadeId = item.getValue();
580 logger.debug("Shade '{}' handler not initialized", shadeId);