]> git.basschouten.com Git - openhab-addons.git/blob
0827a682e31e40855df9dd9f6d1e4e9d947d5448
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.hdpowerview.internal.handler;
14
15 import java.util.ArrayList;
16 import java.util.HashMap;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.Map.Entry;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
23 import javax.ws.rs.ProcessingException;
24
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;
53
54 import com.google.gson.JsonParseException;
55
56 /**
57  * The {@link HDPowerViewHubHandler} is responsible for handling commands, which
58  * are sent to one of the channels.
59  *
60  * @author Andy Lintner - Initial contribution
61  * @author Andrew Fiddian-Green - Added support for secondary rail positions
62  */
63 @NonNullByDefault
64 public class HDPowerViewHubHandler extends BaseBridgeHandler {
65
66     private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubHandler.class);
67     private final HttpClient httpClient;
68
69     private long refreshInterval;
70     private long hardRefreshInterval;
71
72     private @Nullable HDPowerViewWebTargets webTargets;
73     private @Nullable ScheduledFuture<?> pollFuture;
74     private @Nullable ScheduledFuture<?> hardRefreshFuture;
75
76     private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
77             HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE);
78
79     public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient) {
80         super(bridge);
81         this.httpClient = httpClient;
82     }
83
84     @Override
85     public void handleCommand(ChannelUID channelUID, Command command) {
86         if (RefreshType.REFRESH.equals(command)) {
87             requestRefreshShades();
88             return;
89         }
90
91         Channel channel = getThing().getChannel(channelUID.getId());
92         if (channel != null && sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
93             if (OnOffType.ON.equals(command)) {
94                 try {
95                     HDPowerViewWebTargets webTargets = this.webTargets;
96                     if (webTargets == null) {
97                         throw new ProcessingException("Web targets not initialized");
98                     }
99                     webTargets.activateScene(Integer.parseInt(channelUID.getId()));
100                 } catch (HubMaintenanceException e) {
101                     // exceptions are logged in HDPowerViewWebTargets
102                 } catch (NumberFormatException | HubProcessingException e) {
103                     logger.debug("Unexpected error {}", e.getMessage());
104                 }
105             }
106         }
107     }
108
109     @Override
110     public void initialize() {
111         logger.debug("Initializing hub");
112         HDPowerViewHubConfiguration config = getConfigAs(HDPowerViewHubConfiguration.class);
113         String host = config.host;
114
115         if (host == null || host.isEmpty()) {
116             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Host address must be set");
117             return;
118         }
119
120         webTargets = new HDPowerViewWebTargets(httpClient, host);
121         refreshInterval = config.refresh;
122         hardRefreshInterval = config.hardRefresh;
123         schedulePoll();
124     }
125
126     public @Nullable HDPowerViewWebTargets getWebTargets() {
127         return webTargets;
128     }
129
130     @Override
131     public void handleRemoval() {
132         super.handleRemoval();
133         stopPoll();
134     }
135
136     @Override
137     public void dispose() {
138         super.dispose();
139         stopPoll();
140     }
141
142     private void schedulePoll() {
143         ScheduledFuture<?> future = this.pollFuture;
144         if (future != null) {
145             future.cancel(false);
146         }
147         logger.debug("Scheduling poll for 5000ms out, then every {}ms", refreshInterval);
148         this.pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 5000, refreshInterval, TimeUnit.MILLISECONDS);
149
150         future = this.hardRefreshFuture;
151         if (future != null) {
152             future.cancel(false);
153         }
154         if (hardRefreshInterval > 0) {
155             logger.debug("Scheduling hard refresh every {}minutes", hardRefreshInterval);
156             this.hardRefreshFuture = scheduler.scheduleWithFixedDelay(this::requestRefreshShades, 1,
157                     hardRefreshInterval, TimeUnit.MINUTES);
158         }
159     }
160
161     private synchronized void stopPoll() {
162         ScheduledFuture<?> future = this.pollFuture;
163         if (future != null) {
164             future.cancel(true);
165         }
166         this.pollFuture = null;
167
168         future = this.hardRefreshFuture;
169         if (future != null) {
170             future.cancel(true);
171         }
172         this.hardRefreshFuture = null;
173     }
174
175     private synchronized void poll() {
176         try {
177             logger.debug("Polling for state");
178             pollShades();
179             pollScenes();
180         } catch (JsonParseException e) {
181             logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
182         } catch (HubProcessingException e) {
183             logger.warn("Error connecting to bridge: {}", e.getMessage());
184             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, e.getMessage());
185         } catch (HubMaintenanceException e) {
186             // exceptions are logged in HDPowerViewWebTargets
187         }
188     }
189
190     private void pollShades() throws JsonParseException, HubProcessingException, HubMaintenanceException {
191         HDPowerViewWebTargets webTargets = this.webTargets;
192         if (webTargets == null) {
193             throw new ProcessingException("Web targets not initialized");
194         }
195
196         Shades shades = webTargets.getShades();
197         if (shades == null) {
198             throw new JsonParseException("Missing 'shades' element");
199         }
200
201         List<ShadeData> shadesData = shades.shadeData;
202         if (shadesData == null) {
203             throw new JsonParseException("Missing 'shades.shadeData' element");
204         }
205
206         updateStatus(ThingStatus.ONLINE);
207         logger.debug("Received data for {} shades", shadesData.size());
208
209         Map<String, ShadeData> idShadeDataMap = getIdShadeDataMap(shadesData);
210         Map<Thing, String> thingIdMap = getThingIdMap();
211         for (Entry<Thing, String> item : thingIdMap.entrySet()) {
212             Thing thing = item.getKey();
213             String shadeId = item.getValue();
214             ShadeData shadeData = idShadeDataMap.get(shadeId);
215             updateShadeThing(shadeId, thing, shadeData);
216         }
217     }
218
219     private void updateShadeThing(String shadeId, Thing thing, @Nullable ShadeData shadeData) {
220         HDPowerViewShadeHandler thingHandler = ((HDPowerViewShadeHandler) thing.getHandler());
221         if (thingHandler == null) {
222             logger.debug("Shade '{}' handler not initialized", shadeId);
223             return;
224         }
225         if (shadeData == null) {
226             logger.debug("Shade '{}' has no data in hub", shadeId);
227         } else {
228             logger.debug("Updating shade '{}'", shadeId);
229         }
230         thingHandler.onReceiveUpdate(shadeData);
231     }
232
233     private void pollScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException {
234         HDPowerViewWebTargets webTargets = this.webTargets;
235         if (webTargets == null) {
236             throw new ProcessingException("Web targets not initialized");
237         }
238
239         Scenes scenes = webTargets.getScenes();
240         if (scenes == null) {
241             throw new JsonParseException("Missing 'scenes' element");
242         }
243
244         List<Scene> sceneData = scenes.sceneData;
245         if (sceneData == null) {
246             throw new JsonParseException("Missing 'scenes.sceneData' element");
247         }
248         logger.debug("Received data for {} scenes", sceneData.size());
249
250         Map<String, Channel> idChannelMap = getIdChannelMap();
251         for (Scene scene : sceneData) {
252             // remove existing scene channel from the map
253             String sceneId = Integer.toString(scene.id);
254             if (idChannelMap.containsKey(sceneId)) {
255                 idChannelMap.remove(sceneId);
256                 logger.debug("Keeping channel for existing scene '{}'", sceneId);
257             } else {
258                 // create a new scene channel
259                 ChannelUID channelUID = new ChannelUID(getThing().getUID(), sceneId);
260                 Channel channel = ChannelBuilder.create(channelUID, "Switch").withType(sceneChannelTypeUID)
261                         .withLabel(scene.getName()).withDescription("Activates the scene " + scene.getName()).build();
262                 updateThing(editThing().withChannel(channel).build());
263                 logger.debug("Creating new channel for scene '{}'", sceneId);
264             }
265         }
266
267         // remove any previously created channels that no longer exist
268         if (!idChannelMap.isEmpty()) {
269             logger.debug("Removing {} orphan scene channels", idChannelMap.size());
270             List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
271             allChannels.removeAll(idChannelMap.values());
272             updateThing(editThing().withChannels(allChannels).build());
273         }
274     }
275
276     private Map<Thing, String> getThingIdMap() {
277         Map<Thing, String> ret = new HashMap<>();
278         for (Thing thing : getThing().getThings()) {
279             String id = thing.getConfiguration().as(HDPowerViewShadeConfiguration.class).id;
280             if (id != null && !id.isEmpty()) {
281                 ret.put(thing, id);
282             }
283         }
284         return ret;
285     }
286
287     private Map<String, ShadeData> getIdShadeDataMap(List<ShadeData> shadeData) {
288         Map<String, ShadeData> ret = new HashMap<>();
289         for (ShadeData shade : shadeData) {
290             if (shade.id != 0) {
291                 ret.put(Integer.toString(shade.id), shade);
292             }
293         }
294         return ret;
295     }
296
297     private Map<String, Channel> getIdChannelMap() {
298         Map<String, Channel> ret = new HashMap<>();
299         for (Channel channel : getThing().getChannels()) {
300             if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
301                 ret.put(channel.getUID().getId(), channel);
302             }
303         }
304         return ret;
305     }
306
307     private void requestRefreshShades() {
308         Map<Thing, String> thingIdMap = getThingIdMap();
309         for (Entry<Thing, String> item : thingIdMap.entrySet()) {
310             Thing thing = item.getKey();
311             ThingHandler handler = thing.getHandler();
312             if (handler instanceof HDPowerViewShadeHandler) {
313                 ((HDPowerViewShadeHandler) handler).requestRefreshShade();
314             } else {
315                 String shadeId = item.getValue();
316                 logger.debug("Shade '{}' handler not initialized", shadeId);
317             }
318         }
319     }
320 }