2 * Copyright (c) 2010-2021 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.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import javax.ws.rs.ProcessingException;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
29 import org.openhab.binding.hdpowerview.internal.HDPowerViewTranslationProvider;
30 import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
31 import org.openhab.binding.hdpowerview.internal.HubMaintenanceException;
32 import org.openhab.binding.hdpowerview.internal.HubProcessingException;
33 import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections;
34 import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection;
35 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
36 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene;
37 import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
38 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
39 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewHubConfiguration;
40 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
41 import org.openhab.core.library.types.OnOffType;
42 import org.openhab.core.thing.Bridge;
43 import org.openhab.core.thing.Channel;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.binding.BaseBridgeHandler;
49 import org.openhab.core.thing.binding.ThingHandler;
50 import org.openhab.core.thing.binding.builder.ChannelBuilder;
51 import org.openhab.core.thing.type.ChannelTypeUID;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.RefreshType;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
57 import com.google.gson.JsonParseException;
60 * The {@link HDPowerViewHubHandler} is responsible for handling commands, which
61 * are sent to one of the channels.
63 * @author Andy Lintner - Initial contribution
64 * @author Andrew Fiddian-Green - Added support for secondary rail positions
65 * @author Jacob Laursen - Add support for scene groups
68 public class HDPowerViewHubHandler extends BaseBridgeHandler {
70 private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubHandler.class);
71 private final HttpClient httpClient;
72 private final HDPowerViewTranslationProvider translationProvider;
74 private long refreshInterval;
75 private long hardRefreshPositionInterval;
76 private long hardRefreshBatteryLevelInterval;
78 private @Nullable HDPowerViewWebTargets webTargets;
79 private @Nullable ScheduledFuture<?> pollFuture;
80 private @Nullable ScheduledFuture<?> hardRefreshPositionFuture;
81 private @Nullable ScheduledFuture<?> hardRefreshBatteryLevelFuture;
83 private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
84 HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE);
86 private final ChannelTypeUID sceneCollectionChannelTypeUID = new ChannelTypeUID(
87 HDPowerViewBindingConstants.BINDING_ID, HDPowerViewBindingConstants.CHANNELTYPE_SCENE_GROUP_ACTIVATE);
89 public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient,
90 HDPowerViewTranslationProvider translationProvider) {
92 this.httpClient = httpClient;
93 this.translationProvider = translationProvider;
97 public void handleCommand(ChannelUID channelUID, Command command) {
98 if (RefreshType.REFRESH.equals(command)) {
99 requestRefreshShadePositions();
103 if (!OnOffType.ON.equals(command)) {
107 Channel channel = getThing().getChannel(channelUID.getId());
108 if (channel == null) {
113 HDPowerViewWebTargets webTargets = this.webTargets;
114 if (webTargets == null) {
115 throw new ProcessingException("Web targets not initialized");
117 int id = Integer.parseInt(channelUID.getId());
118 if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
119 webTargets.activateScene(id);
120 } else if (sceneCollectionChannelTypeUID.equals(channel.getChannelTypeUID())) {
121 webTargets.activateSceneCollection(id);
123 } catch (HubMaintenanceException e) {
124 // exceptions are logged in HDPowerViewWebTargets
125 } catch (NumberFormatException | HubProcessingException e) {
126 logger.debug("Unexpected error {}", e.getMessage());
131 public void initialize() {
132 logger.debug("Initializing hub");
133 HDPowerViewHubConfiguration config = getConfigAs(HDPowerViewHubConfiguration.class);
134 String host = config.host;
136 if (host == null || host.isEmpty()) {
137 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
138 "@text/offline.conf-error.no-host-address");
142 webTargets = new HDPowerViewWebTargets(httpClient, host);
143 refreshInterval = config.refresh;
144 hardRefreshPositionInterval = config.hardRefresh;
145 hardRefreshBatteryLevelInterval = config.hardRefreshBatteryLevel;
149 public @Nullable HDPowerViewWebTargets getWebTargets() {
154 public void handleRemoval() {
155 super.handleRemoval();
160 public void dispose() {
165 private void schedulePoll() {
166 ScheduledFuture<?> future = this.pollFuture;
167 if (future != null) {
168 future.cancel(false);
170 logger.debug("Scheduling poll for 5000ms out, then every {}ms", refreshInterval);
171 this.pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 5000, refreshInterval, TimeUnit.MILLISECONDS);
173 future = this.hardRefreshPositionFuture;
174 if (future != null) {
175 future.cancel(false);
177 if (hardRefreshPositionInterval > 0) {
178 logger.debug("Scheduling hard position refresh every {} minutes", hardRefreshPositionInterval);
179 this.hardRefreshPositionFuture = scheduler.scheduleWithFixedDelay(this::requestRefreshShadePositions, 1,
180 hardRefreshPositionInterval, TimeUnit.MINUTES);
183 future = this.hardRefreshBatteryLevelFuture;
184 if (future != null) {
185 future.cancel(false);
187 if (hardRefreshBatteryLevelInterval > 0) {
188 logger.debug("Scheduling hard battery level refresh every {} hours", hardRefreshBatteryLevelInterval);
189 this.hardRefreshBatteryLevelFuture = scheduler.scheduleWithFixedDelay(
190 this::requestRefreshShadeBatteryLevels, 1, hardRefreshBatteryLevelInterval, TimeUnit.HOURS);
194 private synchronized void stopPoll() {
195 ScheduledFuture<?> future = this.pollFuture;
196 if (future != null) {
199 this.pollFuture = null;
201 future = this.hardRefreshPositionFuture;
202 if (future != null) {
205 this.hardRefreshPositionFuture = null;
207 future = this.hardRefreshBatteryLevelFuture;
208 if (future != null) {
211 this.hardRefreshBatteryLevelFuture = null;
214 private synchronized void poll() {
216 logger.debug("Polling for state");
219 pollSceneCollections();
220 } catch (JsonParseException e) {
221 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
222 } catch (HubProcessingException e) {
223 logger.warn("Error connecting to bridge: {}", e.getMessage());
224 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, e.getMessage());
225 } catch (HubMaintenanceException e) {
226 // exceptions are logged in HDPowerViewWebTargets
230 private void pollShades() throws JsonParseException, HubProcessingException, HubMaintenanceException {
231 HDPowerViewWebTargets webTargets = this.webTargets;
232 if (webTargets == null) {
233 throw new ProcessingException("Web targets not initialized");
236 Shades shades = webTargets.getShades();
237 if (shades == null) {
238 throw new JsonParseException("Missing 'shades' element");
241 List<ShadeData> shadesData = shades.shadeData;
242 if (shadesData == null) {
243 throw new JsonParseException("Missing 'shades.shadeData' element");
246 updateStatus(ThingStatus.ONLINE);
247 logger.debug("Received data for {} shades", shadesData.size());
249 Map<String, ShadeData> idShadeDataMap = getIdShadeDataMap(shadesData);
250 Map<Thing, String> thingIdMap = getThingIdMap();
251 for (Entry<Thing, String> item : thingIdMap.entrySet()) {
252 Thing thing = item.getKey();
253 String shadeId = item.getValue();
254 ShadeData shadeData = idShadeDataMap.get(shadeId);
255 updateShadeThing(shadeId, thing, shadeData);
259 private void updateShadeThing(String shadeId, Thing thing, @Nullable ShadeData shadeData) {
260 HDPowerViewShadeHandler thingHandler = ((HDPowerViewShadeHandler) thing.getHandler());
261 if (thingHandler == null) {
262 logger.debug("Shade '{}' handler not initialized", shadeId);
265 if (shadeData == null) {
266 logger.debug("Shade '{}' has no data in hub", shadeId);
268 logger.debug("Updating shade '{}'", shadeId);
270 thingHandler.onReceiveUpdate(shadeData);
273 private void pollScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException {
274 HDPowerViewWebTargets webTargets = this.webTargets;
275 if (webTargets == null) {
276 throw new ProcessingException("Web targets not initialized");
279 Scenes scenes = webTargets.getScenes();
280 if (scenes == null) {
281 throw new JsonParseException("Missing 'scenes' element");
284 List<Scene> sceneData = scenes.sceneData;
285 if (sceneData == null) {
286 throw new JsonParseException("Missing 'scenes.sceneData' element");
288 logger.debug("Received data for {} scenes", sceneData.size());
290 Map<String, Channel> idChannelMap = getIdSceneChannelMap();
291 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
292 boolean isChannelListChanged = false;
293 for (Scene scene : sceneData) {
294 // remove existing scene channel from the map
295 String sceneId = Integer.toString(scene.id);
296 if (idChannelMap.containsKey(sceneId)) {
297 idChannelMap.remove(sceneId);
298 logger.debug("Keeping channel for existing scene '{}'", sceneId);
300 // create a new scene channel
301 ChannelUID channelUID = new ChannelUID(getThing().getUID(), sceneId);
302 String description = translationProvider.getText("dynamic-channel.scene-activate.description",
304 Channel channel = ChannelBuilder.create(channelUID, "Switch").withType(sceneChannelTypeUID)
305 .withLabel(scene.getName()).withDescription(description).build();
306 allChannels.add(channel);
307 isChannelListChanged = true;
308 logger.debug("Creating new channel for scene '{}'", sceneId);
312 // remove any previously created channels that no longer exist
313 if (!idChannelMap.isEmpty()) {
314 logger.debug("Removing {} orphan scene channels", idChannelMap.size());
315 allChannels.removeAll(idChannelMap.values());
316 isChannelListChanged = true;
319 if (isChannelListChanged) {
320 updateThing(editThing().withChannels(allChannels).build());
324 private void pollSceneCollections() throws JsonParseException, HubProcessingException, HubMaintenanceException {
325 HDPowerViewWebTargets webTargets = this.webTargets;
326 if (webTargets == null) {
327 throw new ProcessingException("Web targets not initialized");
330 SceneCollections sceneCollections = webTargets.getSceneCollections();
331 if (sceneCollections == null) {
332 throw new JsonParseException("Missing 'sceneCollections' element");
335 List<SceneCollection> sceneCollectionData = sceneCollections.sceneCollectionData;
336 if (sceneCollectionData == null) {
337 throw new JsonParseException("Missing 'sceneCollections.sceneCollectionData' element");
339 logger.debug("Received data for {} sceneCollections", sceneCollectionData.size());
341 Map<String, Channel> idChannelMap = getIdSceneCollectionChannelMap();
342 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
343 boolean isChannelListChanged = false;
344 for (SceneCollection sceneCollection : sceneCollectionData) {
345 // remove existing scene collection channel from the map
346 String sceneCollectionId = Integer.toString(sceneCollection.id);
347 if (idChannelMap.containsKey(sceneCollectionId)) {
348 idChannelMap.remove(sceneCollectionId);
349 logger.debug("Keeping channel for existing scene collection '{}'", sceneCollectionId);
351 // create a new scene collection channel
352 ChannelUID channelUID = new ChannelUID(getThing().getUID(), sceneCollectionId);
353 String description = translationProvider.getText("dynamic-channel.scene-group-activate.description",
354 sceneCollection.getName());
355 Channel channel = ChannelBuilder.create(channelUID, "Switch").withType(sceneCollectionChannelTypeUID)
356 .withLabel(sceneCollection.getName()).withDescription(description).build();
357 allChannels.add(channel);
358 isChannelListChanged = true;
359 logger.debug("Creating new channel for scene collection '{}'", sceneCollectionId);
363 // remove any previously created channels that no longer exist
364 if (!idChannelMap.isEmpty()) {
365 logger.debug("Removing {} orphan scene collection channels", idChannelMap.size());
366 allChannels.removeAll(idChannelMap.values());
367 isChannelListChanged = true;
370 if (isChannelListChanged) {
371 updateThing(editThing().withChannels(allChannels).build());
375 private Map<Thing, String> getThingIdMap() {
376 Map<Thing, String> ret = new HashMap<>();
377 for (Thing thing : getThing().getThings()) {
378 String id = thing.getConfiguration().as(HDPowerViewShadeConfiguration.class).id;
379 if (id != null && !id.isEmpty()) {
386 private Map<String, ShadeData> getIdShadeDataMap(List<ShadeData> shadeData) {
387 Map<String, ShadeData> ret = new HashMap<>();
388 for (ShadeData shade : shadeData) {
390 ret.put(Integer.toString(shade.id), shade);
396 private Map<String, Channel> getIdSceneChannelMap() {
397 Map<String, Channel> ret = new HashMap<>();
398 for (Channel channel : getThing().getChannels()) {
399 if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
400 ret.put(channel.getUID().getId(), channel);
406 private Map<String, Channel> getIdSceneCollectionChannelMap() {
407 Map<String, Channel> ret = new HashMap<>();
408 for (Channel channel : getThing().getChannels()) {
409 if (sceneCollectionChannelTypeUID.equals(channel.getChannelTypeUID())) {
410 ret.put(channel.getUID().getId(), channel);
416 private void requestRefreshShadePositions() {
417 Map<Thing, String> thingIdMap = getThingIdMap();
418 for (Entry<Thing, String> item : thingIdMap.entrySet()) {
419 Thing thing = item.getKey();
420 ThingHandler handler = thing.getHandler();
421 if (handler instanceof HDPowerViewShadeHandler) {
422 ((HDPowerViewShadeHandler) handler).requestRefreshShadePosition();
424 String shadeId = item.getValue();
425 logger.debug("Shade '{}' handler not initialized", shadeId);
430 private void requestRefreshShadeBatteryLevels() {
431 Map<Thing, String> thingIdMap = getThingIdMap();
432 for (Entry<Thing, String> item : thingIdMap.entrySet()) {
433 Thing thing = item.getKey();
434 ThingHandler handler = thing.getHandler();
435 if (handler instanceof HDPowerViewShadeHandler) {
436 ((HDPowerViewShadeHandler) handler).requestRefreshShadeBatteryLevel();
438 String shadeId = item.getValue();
439 logger.debug("Shade '{}' handler not initialized", shadeId);