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.HDPowerViewWebTargets;
30 import org.openhab.binding.hdpowerview.internal.HubMaintenanceException;
31 import org.openhab.binding.hdpowerview.internal.HubProcessingException;
32 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
33 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene;
34 import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
35 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
36 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewHubConfiguration;
37 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.thing.Bridge;
40 import org.openhab.core.thing.Channel;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.binding.BaseBridgeHandler;
46 import org.openhab.core.thing.binding.ThingHandler;
47 import org.openhab.core.thing.binding.builder.ChannelBuilder;
48 import org.openhab.core.thing.type.ChannelTypeUID;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.RefreshType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
54 import com.google.gson.JsonParseException;
57 * The {@link HDPowerViewHubHandler} is responsible for handling commands, which
58 * are sent to one of the channels.
60 * @author Andy Lintner - Initial contribution
61 * @author Andrew Fiddian-Green - Added support for secondary rail positions
64 public class HDPowerViewHubHandler extends BaseBridgeHandler {
66 private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubHandler.class);
67 private final HttpClient httpClient;
69 private long refreshInterval;
70 private long hardRefreshPositionInterval;
71 private long hardRefreshBatteryLevelInterval;
73 private @Nullable HDPowerViewWebTargets webTargets;
74 private @Nullable ScheduledFuture<?> pollFuture;
75 private @Nullable ScheduledFuture<?> hardRefreshPositionFuture;
76 private @Nullable ScheduledFuture<?> hardRefreshBatteryLevelFuture;
78 private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
79 HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE);
81 public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient) {
83 this.httpClient = httpClient;
87 public void handleCommand(ChannelUID channelUID, Command command) {
88 if (RefreshType.REFRESH.equals(command)) {
89 requestRefreshShadePositions();
93 Channel channel = getThing().getChannel(channelUID.getId());
94 if (channel != null && sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
95 if (OnOffType.ON.equals(command)) {
97 HDPowerViewWebTargets webTargets = this.webTargets;
98 if (webTargets == null) {
99 throw new ProcessingException("Web targets not initialized");
101 webTargets.activateScene(Integer.parseInt(channelUID.getId()));
102 } catch (HubMaintenanceException e) {
103 // exceptions are logged in HDPowerViewWebTargets
104 } catch (NumberFormatException | HubProcessingException e) {
105 logger.debug("Unexpected error {}", e.getMessage());
112 public void initialize() {
113 logger.debug("Initializing hub");
114 HDPowerViewHubConfiguration config = getConfigAs(HDPowerViewHubConfiguration.class);
115 String host = config.host;
117 if (host == null || host.isEmpty()) {
118 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Host address must be set");
122 webTargets = new HDPowerViewWebTargets(httpClient, host);
123 refreshInterval = config.refresh;
124 hardRefreshPositionInterval = config.hardRefresh;
125 hardRefreshBatteryLevelInterval = config.hardRefreshBatteryLevel;
129 public @Nullable HDPowerViewWebTargets getWebTargets() {
134 public void handleRemoval() {
135 super.handleRemoval();
140 public void dispose() {
145 private void schedulePoll() {
146 ScheduledFuture<?> future = this.pollFuture;
147 if (future != null) {
148 future.cancel(false);
150 logger.debug("Scheduling poll for 5000ms out, then every {}ms", refreshInterval);
151 this.pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 5000, refreshInterval, TimeUnit.MILLISECONDS);
153 future = this.hardRefreshPositionFuture;
154 if (future != null) {
155 future.cancel(false);
157 if (hardRefreshPositionInterval > 0) {
158 logger.debug("Scheduling hard position refresh every {} minutes", hardRefreshPositionInterval);
159 this.hardRefreshPositionFuture = scheduler.scheduleWithFixedDelay(this::requestRefreshShadePositions, 1,
160 hardRefreshPositionInterval, TimeUnit.MINUTES);
163 future = this.hardRefreshBatteryLevelFuture;
164 if (future != null) {
165 future.cancel(false);
167 if (hardRefreshBatteryLevelInterval > 0) {
168 logger.debug("Scheduling hard battery level refresh every {} hours", hardRefreshBatteryLevelInterval);
169 this.hardRefreshBatteryLevelFuture = scheduler.scheduleWithFixedDelay(
170 this::requestRefreshShadeBatteryLevels, 1, hardRefreshBatteryLevelInterval, TimeUnit.HOURS);
174 private synchronized void stopPoll() {
175 ScheduledFuture<?> future = this.pollFuture;
176 if (future != null) {
179 this.pollFuture = null;
181 future = this.hardRefreshPositionFuture;
182 if (future != null) {
185 this.hardRefreshPositionFuture = null;
187 future = this.hardRefreshBatteryLevelFuture;
188 if (future != null) {
191 this.hardRefreshBatteryLevelFuture = null;
194 private synchronized void poll() {
196 logger.debug("Polling for state");
199 } catch (JsonParseException e) {
200 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
201 } catch (HubProcessingException e) {
202 logger.warn("Error connecting to bridge: {}", e.getMessage());
203 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, e.getMessage());
204 } catch (HubMaintenanceException e) {
205 // exceptions are logged in HDPowerViewWebTargets
209 private void pollShades() throws JsonParseException, HubProcessingException, HubMaintenanceException {
210 HDPowerViewWebTargets webTargets = this.webTargets;
211 if (webTargets == null) {
212 throw new ProcessingException("Web targets not initialized");
215 Shades shades = webTargets.getShades();
216 if (shades == null) {
217 throw new JsonParseException("Missing 'shades' element");
220 List<ShadeData> shadesData = shades.shadeData;
221 if (shadesData == null) {
222 throw new JsonParseException("Missing 'shades.shadeData' element");
225 updateStatus(ThingStatus.ONLINE);
226 logger.debug("Received data for {} shades", shadesData.size());
228 Map<String, ShadeData> idShadeDataMap = getIdShadeDataMap(shadesData);
229 Map<Thing, String> thingIdMap = getThingIdMap();
230 for (Entry<Thing, String> item : thingIdMap.entrySet()) {
231 Thing thing = item.getKey();
232 String shadeId = item.getValue();
233 ShadeData shadeData = idShadeDataMap.get(shadeId);
234 updateShadeThing(shadeId, thing, shadeData);
238 private void updateShadeThing(String shadeId, Thing thing, @Nullable ShadeData shadeData) {
239 HDPowerViewShadeHandler thingHandler = ((HDPowerViewShadeHandler) thing.getHandler());
240 if (thingHandler == null) {
241 logger.debug("Shade '{}' handler not initialized", shadeId);
244 if (shadeData == null) {
245 logger.debug("Shade '{}' has no data in hub", shadeId);
247 logger.debug("Updating shade '{}'", shadeId);
249 thingHandler.onReceiveUpdate(shadeData);
252 private void pollScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException {
253 HDPowerViewWebTargets webTargets = this.webTargets;
254 if (webTargets == null) {
255 throw new ProcessingException("Web targets not initialized");
258 Scenes scenes = webTargets.getScenes();
259 if (scenes == null) {
260 throw new JsonParseException("Missing 'scenes' element");
263 List<Scene> sceneData = scenes.sceneData;
264 if (sceneData == null) {
265 throw new JsonParseException("Missing 'scenes.sceneData' element");
267 logger.debug("Received data for {} scenes", sceneData.size());
269 Map<String, Channel> idChannelMap = getIdChannelMap();
270 for (Scene scene : sceneData) {
271 // remove existing scene channel from the map
272 String sceneId = Integer.toString(scene.id);
273 if (idChannelMap.containsKey(sceneId)) {
274 idChannelMap.remove(sceneId);
275 logger.debug("Keeping channel for existing scene '{}'", sceneId);
277 // create a new scene channel
278 ChannelUID channelUID = new ChannelUID(getThing().getUID(), sceneId);
279 Channel channel = ChannelBuilder.create(channelUID, "Switch").withType(sceneChannelTypeUID)
280 .withLabel(scene.getName()).withDescription("Activates the scene " + scene.getName()).build();
281 updateThing(editThing().withChannel(channel).build());
282 logger.debug("Creating new channel for scene '{}'", sceneId);
286 // remove any previously created channels that no longer exist
287 if (!idChannelMap.isEmpty()) {
288 logger.debug("Removing {} orphan scene channels", idChannelMap.size());
289 List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
290 allChannels.removeAll(idChannelMap.values());
291 updateThing(editThing().withChannels(allChannels).build());
295 private Map<Thing, String> getThingIdMap() {
296 Map<Thing, String> ret = new HashMap<>();
297 for (Thing thing : getThing().getThings()) {
298 String id = thing.getConfiguration().as(HDPowerViewShadeConfiguration.class).id;
299 if (id != null && !id.isEmpty()) {
306 private Map<String, ShadeData> getIdShadeDataMap(List<ShadeData> shadeData) {
307 Map<String, ShadeData> ret = new HashMap<>();
308 for (ShadeData shade : shadeData) {
310 ret.put(Integer.toString(shade.id), shade);
316 private Map<String, Channel> getIdChannelMap() {
317 Map<String, Channel> ret = new HashMap<>();
318 for (Channel channel : getThing().getChannels()) {
319 if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
320 ret.put(channel.getUID().getId(), channel);
326 private void requestRefreshShadePositions() {
327 Map<Thing, String> thingIdMap = getThingIdMap();
328 for (Entry<Thing, String> item : thingIdMap.entrySet()) {
329 Thing thing = item.getKey();
330 ThingHandler handler = thing.getHandler();
331 if (handler instanceof HDPowerViewShadeHandler) {
332 ((HDPowerViewShadeHandler) handler).requestRefreshShadePosition();
334 String shadeId = item.getValue();
335 logger.debug("Shade '{}' handler not initialized", shadeId);
340 private void requestRefreshShadeBatteryLevels() {
341 Map<Thing, String> thingIdMap = getThingIdMap();
342 for (Entry<Thing, String> item : thingIdMap.entrySet()) {
343 Thing thing = item.getKey();
344 ThingHandler handler = thing.getHandler();
345 if (handler instanceof HDPowerViewShadeHandler) {
346 ((HDPowerViewShadeHandler) handler).requestRefreshShadeBatteryLevel();
348 String shadeId = item.getValue();
349 logger.debug("Shade '{}' handler not initialized", shadeId);