2 * Copyright (c) 2010-2020 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.hydrawise.internal;
15 import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
17 import java.time.ZonedDateTime;
18 import java.time.temporal.ChronoUnit;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import org.apache.commons.lang.StringUtils;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
27 import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
28 import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
29 import org.openhab.binding.hydrawise.internal.api.model.LocalScheduleResponse;
30 import org.openhab.binding.hydrawise.internal.api.model.Relay;
31 import org.openhab.binding.hydrawise.internal.api.model.Running;
32 import org.openhab.core.library.types.DateTimeType;
33 import org.openhab.core.library.types.DecimalType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.binding.BaseThingHandler;
41 import org.openhab.core.types.Command;
42 import org.openhab.core.types.RefreshType;
43 import org.openhab.core.types.State;
44 import org.openhab.core.types.UnDefType;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * The {@link HydrawiseHandler} is responsible for handling commands, which are
50 * sent to one of the channels.
52 * @author Dan Cunningham - Initial contribution
55 public abstract class HydrawiseHandler extends BaseThingHandler {
57 private final Logger logger = LoggerFactory.getLogger(HydrawiseHandler.class);
58 private @Nullable ScheduledFuture<?> pollFuture;
59 private Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<>());
60 private Map<String, Relay> relayMap = Collections.synchronizedMap(new HashMap<>());
63 * value observed being used by the Hydrawise clients as a max time value,
65 private static long MAX_RUN_TIME = 157680000;
68 * Minimum amount of time we can poll for updates
70 protected static final int MIN_REFRESH_SECONDS = 5;
73 * Minimum amount of time we can poll after a command
75 protected static final int COMMAND_REFRESH_SECONDS = 5;
80 protected int refresh;
83 * Future to poll for updated
86 public HydrawiseHandler(Thing thing) {
90 @SuppressWarnings({ "null", "unused" }) // compiler does not like relayMap.get can return null
92 public void handleCommand(ChannelUID channelUID, Command command) {
93 if (getThing().getStatus() != ThingStatus.ONLINE) {
94 logger.warn("Controller is NOT ONLINE and is not responding to commands");
98 // remove our cached state for this, will be safely updated on next poll
99 stateMap.remove(channelUID.getAsString());
101 if (command instanceof RefreshType) {
102 // we already removed this from the cache
106 String group = channelUID.getGroupId();
107 String channelId = channelUID.getIdWithoutGroup();
108 boolean allCommand = CHANNEL_GROUP_ALLZONES.equals(group);
110 Relay relay = relayMap.get(group);
111 if (!allCommand && relay == null) {
112 logger.debug("Zone not found {}", group);
119 case CHANNEL_ZONE_RUN_CUSTOM:
120 if (!(command instanceof DecimalType)) {
121 logger.warn("Invalid command type for run custom {}", command.getClass().getName());
125 sendRunAllCommand(((DecimalType) command).intValue());
127 Objects.requireNonNull(relay);
128 sendRunCommand(((DecimalType) command).intValue(), relay);
131 case CHANNEL_ZONE_RUN:
132 if (!(command instanceof OnOffType)) {
133 logger.warn("Invalid command type for run {}", command.getClass().getName());
137 if (command == OnOffType.ON) {
140 sendStopAllCommand();
143 Objects.requireNonNull(relay);
144 if (command == OnOffType.ON) {
145 sendRunCommand(relay);
147 sendStopCommand(relay);
152 initPolling(COMMAND_REFRESH_SECONDS);
153 } catch (HydrawiseCommandException | HydrawiseConnectionException e) {
154 logger.debug("Could not issue command", e);
155 initPolling(COMMAND_REFRESH_SECONDS);
156 } catch (HydrawiseAuthenticationException e) {
157 logger.debug("Credentials not valid");
158 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
164 public void initialize() {
165 scheduler.schedule(this::configureInternal, 0, TimeUnit.SECONDS);
169 public void dispose() {
170 logger.debug("Handler disposed.");
175 public void channelLinked(ChannelUID channelUID) {
176 // clear our cached value so the new channel gets updated on the next poll
177 stateMap.remove(channelUID.getId());
180 protected abstract void configure()
181 throws NotConfiguredException, HydrawiseConnectionException, HydrawiseAuthenticationException;
183 protected abstract void pollController() throws HydrawiseConnectionException, HydrawiseAuthenticationException;
185 protected abstract void sendRunCommand(int seconds, Relay relay)
186 throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
188 protected abstract void sendRunCommand(Relay relay)
189 throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
191 protected abstract void sendStopCommand(Relay relay)
192 throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
194 protected abstract void sendRunAllCommand()
195 throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
197 protected abstract void sendRunAllCommand(int seconds)
198 throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
200 protected abstract void sendStopAllCommand()
201 throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
203 protected void updateZones(LocalScheduleResponse status) {
204 ZonedDateTime now = ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS);
205 status.relays.forEach(r -> {
206 String group = "zone" + r.getRelayNumber();
207 relayMap.put(group, r);
208 logger.trace("Updateing Zone {} {} ", group, r.name);
209 updateGroupState(group, CHANNEL_ZONE_NAME, new StringType(r.name));
210 updateGroupState(group, CHANNEL_ZONE_TYPE, new DecimalType(r.type));
211 updateGroupState(group, CHANNEL_ZONE_TIME,
212 r.runTimeSeconds != null ? new DecimalType(r.runTimeSeconds) : UnDefType.UNDEF);
213 if (StringUtils.isNotBlank(r.icon)) {
214 updateGroupState(group, CHANNEL_ZONE_ICON, new StringType(BASE_IMAGE_URL + r.icon));
216 if (r.time >= MAX_RUN_TIME) {
217 updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME, UnDefType.UNDEF);
219 updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME,
220 new DateTimeType(now.plusSeconds(r.time).truncatedTo(ChronoUnit.MINUTES)));
223 Optional<Running> running = status.running.stream()
224 .filter(z -> Integer.parseInt(z.relayId) == r.relayId.intValue()).findAny();
225 if (running.isPresent()) {
226 updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.ON);
227 updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(running.get().timeLeft));
228 logger.debug("{} Time Left {}", r.name, running.get().timeLeft);
231 updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.OFF);
232 updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(0));
236 updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_RUN,
237 !status.running.isEmpty() ? OnOffType.ON : OnOffType.OFF);
241 protected void updateGroupState(String group, String channelID, State state) {
242 String channelName = group + "#" + channelID;
243 State oldState = stateMap.put(channelName, state);
244 if (!state.equals(oldState)) {
245 ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelName);
246 logger.debug("updateState updating {} {}", channelUID, state);
247 updateState(channelUID, state);
251 @SuppressWarnings("serial")
253 protected class NotConfiguredException extends Exception {
254 NotConfiguredException(String message) {
259 private boolean isFutureValid(@Nullable ScheduledFuture<?> future) {
260 return future != null && !future.isCancelled();
263 private void configureInternal() {
270 } catch (NotConfiguredException e) {
271 logger.debug("Configuration error {}", e.getMessage());
272 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
273 } catch (HydrawiseConnectionException e) {
274 logger.debug("Could not connect to service");
275 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
276 } catch (HydrawiseAuthenticationException e) {
277 logger.debug("Credentials not valid");
278 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
283 * Starts/Restarts polling with an initial delay. This allows changes in the poll cycle for when commands are sent
284 * and we need to poll sooner then the next refresh cycle.
286 private synchronized void initPolling(int initalDelay) {
288 pollFuture = scheduler.scheduleWithFixedDelay(this::pollControllerInternal, initalDelay, refresh,
293 * Stops/clears this thing's polling future
295 private void clearPolling() {
296 ScheduledFuture<?> localFuture = pollFuture;
297 if (isFutureValid(localFuture)) {
298 if (localFuture != null) {
299 localFuture.cancel(false);
305 * Poll the controller for updates.
307 private void pollControllerInternal() {
310 if (getThing().getStatus() != ThingStatus.ONLINE) {
311 updateStatus(ThingStatus.ONLINE);
313 } catch (HydrawiseConnectionException e) {
314 // poller will continue to run, set offline until next run
315 logger.debug("Exception polling", e);
316 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
317 } catch (HydrawiseAuthenticationException e) {
318 // if are creds are not valid, we need to try re authorizing again
319 logger.debug("Authorization exception during polling", e);