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.hydrawise.internal;
15 import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
17 import java.time.ZonedDateTime;
18 import java.time.temporal.ChronoUnit;
19 import java.util.Collections;
20 import java.util.HashMap;
22 import java.util.Objects;
23 import java.util.Optional;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
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;
52 * The {@link HydrawiseHandler} is responsible for handling commands, which are
53 * sent to one of the channels.
55 * @author Dan Cunningham - Initial contribution
58 public abstract class HydrawiseHandler extends BaseThingHandler {
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<>());
66 * value observed being used by the Hydrawise clients as a max time value,
68 private static long MAX_RUN_TIME = 157680000;
71 * Minimum amount of time we can poll for updates
73 protected static final int MIN_REFRESH_SECONDS = 5;
76 * Minimum amount of time we can poll after a command
78 protected static final int COMMAND_REFRESH_SECONDS = 5;
83 protected int refresh;
86 * Future to poll for updated
89 public HydrawiseHandler(Thing thing) {
93 @SuppressWarnings({ "null", "unused" }) // compiler does not like relayMap.get can return null
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");
101 // remove our cached state for this, will be safely updated on next poll
102 stateMap.remove(channelUID.getAsString());
104 if (command instanceof RefreshType) {
105 // we already removed this from the cache
109 String group = channelUID.getGroupId();
110 String channelId = channelUID.getIdWithoutGroup();
111 boolean allCommand = CHANNEL_GROUP_ALLZONES.equals(group);
113 Relay relay = relayMap.get(group);
114 if (!allCommand && relay == null) {
115 logger.debug("Zone not found {}", group);
122 case CHANNEL_ZONE_RUN_CUSTOM:
123 if (!(command instanceof DecimalType)) {
124 logger.warn("Invalid command type for run custom {}", command.getClass().getName());
128 sendRunAllCommand(((DecimalType) command).intValue());
130 Objects.requireNonNull(relay);
131 sendRunCommand(((DecimalType) command).intValue(), relay);
134 case CHANNEL_ZONE_RUN:
135 if (!(command instanceof OnOffType)) {
136 logger.warn("Invalid command type for run {}", command.getClass().getName());
140 if (command == OnOffType.ON) {
143 sendStopAllCommand();
146 Objects.requireNonNull(relay);
147 if (command == OnOffType.ON) {
148 sendRunCommand(relay);
150 sendStopCommand(relay);
155 initPolling(COMMAND_REFRESH_SECONDS);
156 } catch (HydrawiseCommandException | HydrawiseConnectionException e) {
157 logger.debug("Could not issue command", e);
158 initPolling(COMMAND_REFRESH_SECONDS);
159 } catch (HydrawiseAuthenticationException e) {
160 logger.debug("Credentials not valid");
161 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
167 public void initialize() {
168 scheduler.schedule(this::configureInternal, 0, TimeUnit.SECONDS);
172 public void dispose() {
173 logger.debug("Handler disposed.");
178 public void channelLinked(ChannelUID channelUID) {
179 // clear our cached value so the new channel gets updated on the next poll
180 stateMap.remove(channelUID.getId());
183 protected abstract void configure()
184 throws NotConfiguredException, HydrawiseConnectionException, HydrawiseAuthenticationException;
186 protected abstract void pollController() throws HydrawiseConnectionException, HydrawiseAuthenticationException;
188 protected abstract void sendRunCommand(int seconds, Relay relay)
189 throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
191 protected abstract void sendRunCommand(Relay relay)
192 throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
194 protected abstract void sendStopCommand(Relay relay)
195 throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
197 protected abstract void sendRunAllCommand()
198 throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
200 protected abstract void sendRunAllCommand(int seconds)
201 throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
203 protected abstract void sendStopAllCommand()
204 throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
206 protected void updateZones(LocalScheduleResponse status) {
207 ZonedDateTime now = ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS);
208 status.relays.forEach(r -> {
209 String group = "zone" + r.getRelayNumber();
210 relayMap.put(group, r);
211 logger.trace("Updateing Zone {} {} ", group, r.name);
212 updateGroupState(group, CHANNEL_ZONE_NAME, new StringType(r.name));
213 updateGroupState(group, CHANNEL_ZONE_TYPE, new DecimalType(r.type));
214 updateGroupState(group, CHANNEL_ZONE_TIME,
215 r.runTimeSeconds != null ? new DecimalType(r.runTimeSeconds) : UnDefType.UNDEF);
216 String icon = r.icon;
217 if (icon != null && !icon.isBlank()) {
218 updateGroupState(group, CHANNEL_ZONE_ICON, new StringType(BASE_IMAGE_URL + icon));
220 if (r.time >= MAX_RUN_TIME) {
221 updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME, UnDefType.UNDEF);
223 updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME,
224 new DateTimeType(now.plusSeconds(r.time).truncatedTo(ChronoUnit.MINUTES)));
227 Optional<Running> running = status.running.stream()
228 .filter(z -> Integer.parseInt(z.relayId) == r.relayId.intValue()).findAny();
229 if (running.isPresent()) {
230 updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.ON);
231 updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(running.get().timeLeft));
232 logger.debug("{} Time Left {}", r.name, running.get().timeLeft);
235 updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.OFF);
236 updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(0));
240 updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_RUN,
241 !status.running.isEmpty() ? OnOffType.ON : OnOffType.OFF);
245 protected void updateGroupState(String group, String channelID, State state) {
246 String channelName = group + "#" + channelID;
247 State oldState = stateMap.put(channelName, state);
248 if (!state.equals(oldState)) {
249 ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelName);
250 logger.debug("updateState updating {} {}", channelUID, state);
251 updateState(channelUID, state);
255 @SuppressWarnings("serial")
257 protected class NotConfiguredException extends Exception {
258 NotConfiguredException(String message) {
263 private boolean isFutureValid(@Nullable ScheduledFuture<?> future) {
264 return future != null && !future.isCancelled();
267 private void configureInternal() {
274 } catch (NotConfiguredException e) {
275 logger.debug("Configuration error {}", e.getMessage());
276 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
277 } catch (HydrawiseConnectionException e) {
278 logger.debug("Could not connect to service");
279 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
280 } catch (HydrawiseAuthenticationException e) {
281 logger.debug("Credentials not valid");
282 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
287 * Starts/Restarts polling with an initial delay. This allows changes in the poll cycle for when commands are sent
288 * and we need to poll sooner then the next refresh cycle.
290 private synchronized void initPolling(int initalDelay) {
292 pollFuture = scheduler.scheduleWithFixedDelay(this::pollControllerInternal, initalDelay, refresh,
297 * Stops/clears this thing's polling future
299 private void clearPolling() {
300 ScheduledFuture<?> localFuture = pollFuture;
301 if (isFutureValid(localFuture)) {
302 if (localFuture != null) {
303 localFuture.cancel(false);
309 * Poll the controller for updates.
311 private void pollControllerInternal() {
314 if (getThing().getStatus() != ThingStatus.ONLINE) {
315 updateStatus(ThingStatus.ONLINE);
317 } catch (HydrawiseConnectionException e) {
318 // poller will continue to run, set offline until next run
319 logger.debug("Exception polling", e);
320 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
321 } catch (HydrawiseAuthenticationException e) {
322 // if are creds are not valid, we need to try re authorizing again
323 logger.debug("Authorization exception during polling", e);