]> git.basschouten.com Git - openhab-addons.git/blob
42d4e000b1a49a0508a2e966fab845148245d631
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.hydrawise.internal;
14
15 import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
16
17 import java.time.ZonedDateTime;
18 import java.time.temporal.ChronoUnit;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.Optional;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import org.apache.commons.lang.StringUtils;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
30 import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
31 import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
32 import org.openhab.binding.hydrawise.internal.api.model.LocalScheduleResponse;
33 import org.openhab.binding.hydrawise.internal.api.model.Relay;
34 import org.openhab.binding.hydrawise.internal.api.model.Running;
35 import org.openhab.core.library.types.DateTimeType;
36 import org.openhab.core.library.types.DecimalType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseThingHandler;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.RefreshType;
46 import org.openhab.core.types.State;
47 import org.openhab.core.types.UnDefType;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  * The {@link HydrawiseHandler} is responsible for handling commands, which are
53  * sent to one of the channels.
54  *
55  * @author Dan Cunningham - Initial contribution
56  */
57 @NonNullByDefault
58 public abstract class HydrawiseHandler extends BaseThingHandler {
59
60     private final Logger logger = LoggerFactory.getLogger(HydrawiseHandler.class);
61     private @Nullable ScheduledFuture<?> pollFuture;
62     private Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<>());
63     private Map<String, Relay> relayMap = Collections.synchronizedMap(new HashMap<>());
64
65     /**
66      * value observed being used by the Hydrawise clients as a max time value,
67      */
68     private static long MAX_RUN_TIME = 157680000;
69
70     /**
71      * Minimum amount of time we can poll for updates
72      */
73     protected static final int MIN_REFRESH_SECONDS = 5;
74
75     /**
76      * Minimum amount of time we can poll after a command
77      */
78     protected static final int COMMAND_REFRESH_SECONDS = 5;
79
80     /**
81      * Our poll rate
82      */
83     protected int refresh;
84
85     /**
86      * Future to poll for updated
87      */
88
89     public HydrawiseHandler(Thing thing) {
90         super(thing);
91     }
92
93     @SuppressWarnings({ "null", "unused" }) // compiler does not like relayMap.get can return null
94     @Override
95     public void handleCommand(ChannelUID channelUID, Command command) {
96         if (getThing().getStatus() != ThingStatus.ONLINE) {
97             logger.warn("Controller is NOT ONLINE and is not responding to commands");
98             return;
99         }
100
101         // remove our cached state for this, will be safely updated on next poll
102         stateMap.remove(channelUID.getAsString());
103
104         if (command instanceof RefreshType) {
105             // we already removed this from the cache
106             return;
107         }
108
109         String group = channelUID.getGroupId();
110         String channelId = channelUID.getIdWithoutGroup();
111         boolean allCommand = CHANNEL_GROUP_ALLZONES.equals(group);
112
113         Relay relay = relayMap.get(group);
114         if (!allCommand && relay == null) {
115             logger.debug("Zone not found {}", group);
116             return;
117         }
118
119         try {
120             clearPolling();
121             switch (channelId) {
122                 case CHANNEL_ZONE_RUN_CUSTOM:
123                     if (!(command instanceof DecimalType)) {
124                         logger.warn("Invalid command type for run custom {}", command.getClass().getName());
125                         return;
126                     }
127                     if (allCommand) {
128                         sendRunAllCommand(((DecimalType) command).intValue());
129                     } else {
130                         sendRunCommand(((DecimalType) command).intValue(), relay);
131                     }
132                     break;
133                 case CHANNEL_ZONE_RUN:
134                     if (!(command instanceof OnOffType)) {
135                         logger.warn("Invalid command type for run {}", command.getClass().getName());
136                         return;
137                     }
138                     if (allCommand) {
139                         if (command == OnOffType.ON) {
140                             sendRunAllCommand();
141                         } else {
142                             sendStopAllCommand();
143                         }
144                     } else {
145                         if (command == OnOffType.ON) {
146                             sendRunCommand(relay);
147                         } else {
148                             sendStopCommand(relay);
149                         }
150                     }
151                     break;
152             }
153             initPolling(COMMAND_REFRESH_SECONDS);
154         } catch (HydrawiseCommandException | HydrawiseConnectionException e) {
155             logger.debug("Could not issue command", e);
156             initPolling(COMMAND_REFRESH_SECONDS);
157         } catch (HydrawiseAuthenticationException e) {
158             logger.debug("Credentials not valid");
159             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
160             configureInternal();
161         }
162     }
163
164     @Override
165     public void initialize() {
166         scheduler.schedule(this::configureInternal, 0, TimeUnit.SECONDS);
167     }
168
169     @Override
170     public void dispose() {
171         logger.debug("Handler disposed.");
172         clearPolling();
173     }
174
175     @Override
176     public void channelLinked(ChannelUID channelUID) {
177         // clear our cached value so the new channel gets updated on the next poll
178         stateMap.remove(channelUID.getId());
179     }
180
181     protected abstract void configure()
182             throws NotConfiguredException, HydrawiseConnectionException, HydrawiseAuthenticationException;
183
184     protected abstract void pollController() throws HydrawiseConnectionException, HydrawiseAuthenticationException;
185
186     protected abstract void sendRunCommand(int seconds, Relay relay)
187             throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
188
189     protected abstract void sendRunCommand(Relay relay)
190             throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
191
192     protected abstract void sendStopCommand(Relay relay)
193             throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
194
195     protected abstract void sendRunAllCommand()
196             throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
197
198     protected abstract void sendRunAllCommand(int seconds)
199             throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
200
201     protected abstract void sendStopAllCommand()
202             throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
203
204     protected void updateZones(LocalScheduleResponse status) {
205         ZonedDateTime now = ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS);
206         status.relays.forEach(r -> {
207             String group = "zone" + r.getRelayNumber();
208             relayMap.put(group, r);
209             logger.trace("Updateing Zone {} {} ", group, r.name);
210             updateGroupState(group, CHANNEL_ZONE_NAME, new StringType(r.name));
211             updateGroupState(group, CHANNEL_ZONE_TYPE, new DecimalType(r.type));
212             updateGroupState(group, CHANNEL_ZONE_TIME,
213                     r.runTimeSeconds != null ? new DecimalType(r.runTimeSeconds) : UnDefType.UNDEF);
214             if (StringUtils.isNotBlank(r.icon)) {
215                 updateGroupState(group, CHANNEL_ZONE_ICON, new StringType(BASE_IMAGE_URL + r.icon));
216             }
217             if (r.time >= MAX_RUN_TIME) {
218                 updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME, UnDefType.UNDEF);
219             } else {
220                 updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME,
221                         new DateTimeType(now.plusSeconds(r.time).truncatedTo(ChronoUnit.MINUTES)));
222             }
223
224             Optional<Running> running = status.running.stream()
225                     .filter(z -> Integer.parseInt(z.relayId) == r.relayId.intValue()).findAny();
226             if (running.isPresent()) {
227                 updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.ON);
228                 updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(running.get().timeLeft));
229                 logger.debug("{} Time Left {}", r.name, running.get().timeLeft);
230
231             } else {
232                 updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.OFF);
233                 updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(0));
234
235             }
236
237             updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_RUN,
238                     !status.running.isEmpty() ? OnOffType.ON : OnOffType.OFF);
239         });
240     }
241
242     protected void updateGroupState(String group, String channelID, State state) {
243         String channelName = group + "#" + channelID;
244         State oldState = stateMap.put(channelName, state);
245         if (!state.equals(oldState)) {
246             ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelName);
247             logger.debug("updateState updating {} {}", channelUID, state);
248             updateState(channelUID, state);
249         }
250     }
251
252     @SuppressWarnings("serial")
253     @NonNullByDefault
254     protected class NotConfiguredException extends Exception {
255         NotConfiguredException(String message) {
256             super(message);
257         }
258     }
259
260     private boolean isFutureValid(@Nullable ScheduledFuture<?> future) {
261         return future != null && !future.isCancelled();
262     }
263
264     private void configureInternal() {
265         clearPolling();
266         stateMap.clear();
267         relayMap.clear();
268         try {
269             configure();
270             initPolling(0);
271         } catch (NotConfiguredException e) {
272             logger.debug("Configuration error {}", e.getMessage());
273             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
274         } catch (HydrawiseConnectionException e) {
275             logger.debug("Could not connect to service");
276             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
277         } catch (HydrawiseAuthenticationException e) {
278             logger.debug("Credentials not valid");
279             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
280         }
281     }
282
283     /**
284      * Starts/Restarts polling with an initial delay. This allows changes in the poll cycle for when commands are sent
285      * and we need to poll sooner then the next refresh cycle.
286      */
287     private synchronized void initPolling(int initalDelay) {
288         clearPolling();
289         pollFuture = scheduler.scheduleWithFixedDelay(this::pollControllerInternal, initalDelay, refresh,
290                 TimeUnit.SECONDS);
291     }
292
293     /**
294      * Stops/clears this thing's polling future
295      */
296     private void clearPolling() {
297         ScheduledFuture<?> localFuture = pollFuture;
298         if (isFutureValid(localFuture)) {
299             if (localFuture != null) {
300                 localFuture.cancel(false);
301             }
302         }
303     }
304
305     /**
306      * Poll the controller for updates.
307      */
308     private void pollControllerInternal() {
309         try {
310             pollController();
311             if (getThing().getStatus() != ThingStatus.ONLINE) {
312                 updateStatus(ThingStatus.ONLINE);
313             }
314         } catch (HydrawiseConnectionException e) {
315             // poller will continue to run, set offline until next run
316             logger.debug("Exception polling", e);
317             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
318         } catch (HydrawiseAuthenticationException e) {
319             // if are creds are not valid, we need to try re authorizing again
320             logger.debug("Authorization exception during polling", e);
321             configureInternal();
322         }
323     }
324 }