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