2 * Copyright (c) 2010-2023 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.opengarage.internal;
15 import java.io.IOException;
16 import java.time.Duration;
17 import java.time.Instant;
18 import java.util.concurrent.CompletableFuture;
19 import java.util.concurrent.Future;
20 import java.util.concurrent.TimeUnit;
21 import java.util.function.Function;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.openhab.binding.opengarage.internal.api.ControllerVariables;
25 import org.openhab.binding.opengarage.internal.api.Enums.OpenGarageCommand;
26 import org.openhab.core.library.types.DecimalType;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.library.types.OpenClosedType;
29 import org.openhab.core.library.types.QuantityType;
30 import org.openhab.core.library.types.StopMoveType;
31 import org.openhab.core.library.types.StringType;
32 import org.openhab.core.library.types.UpDownType;
33 import org.openhab.core.library.unit.MetricPrefix;
34 import org.openhab.core.library.unit.SIUnits;
35 import org.openhab.core.thing.Channel;
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.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
46 * The {@link OpenGarageHandler} is responsible for handling commands, which are
47 * sent to one of the channels.
49 * @author Paul Smedley - Initial contribution
50 * @author Dan Cunningham - Minor improvements to vehicle state and invert option
53 public class OpenGarageHandler extends BaseThingHandler {
55 private final Logger logger = LoggerFactory.getLogger(OpenGarageHandler.class);
57 private @NonNullByDefault({}) OpenGarageWebTargets webTargets;
59 // reference to periodically scheduled poll task
60 private Future<?> pollScheduledFuture = CompletableFuture.completedFuture(null);
62 // reference to one-shot poll task which gets scheduled after a garage state change command
63 private Future<?> pollScheduledFutureTransition = CompletableFuture.completedFuture(null);
64 private Instant lastTransition;
65 private String lastTransitionText;
67 private OpenGarageConfiguration config = new OpenGarageConfiguration();
69 public OpenGarageHandler(Thing thing) {
71 this.lastTransition = Instant.MIN;
72 this.lastTransitionText = "";
76 public synchronized void handleCommand(ChannelUID channelUID, Command command) {
78 logger.debug("Received command {} for thing '{}' on channel {}", command, thing.getUID().getAsString(),
80 Function<Boolean, Boolean> maybeInvert = getInverter(channelUID.getId());
81 switch (channelUID.getId()) {
82 case OpenGarageBindingConstants.CHANNEL_OG_STATUS:
83 case OpenGarageBindingConstants.CHANNEL_OG_STATUS_SWITCH:
84 case OpenGarageBindingConstants.CHANNEL_OG_STATUS_ROLLERSHUTTER:
85 if (command.equals(StopMoveType.STOP) || command.equals(StopMoveType.MOVE)) {
86 changeStatus(OpenGarageCommand.CLICK);
88 boolean doorOpen = command.equals(OnOffType.ON) || command.equals(UpDownType.UP);
89 changeStatus(maybeInvert.apply(doorOpen) ? OpenGarageCommand.OPEN : OpenGarageCommand.CLOSE);
90 this.lastTransition = Instant.now();
91 this.lastTransitionText = doorOpen ? this.config.doorOpeningState
92 : this.config.doorClosingState;
94 this.poll(); // invoke poll directly to communicate the door transition state
95 this.pollScheduledFutureTransition.cancel(false);
96 this.pollScheduledFutureTransition = this.scheduler.schedule(this::poll,
97 this.config.doorTransitionTimeSeconds, TimeUnit.SECONDS);
102 } catch (OpenGarageCommunicationException ex) {
103 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage());
108 public void initialize() {
109 this.config = getConfigAs(OpenGarageConfiguration.class);
110 logger.debug("config.hostname = {}, refresh = {}, port = {}", config.hostname, config.refresh, config.port);
111 if (config.hostname.isEmpty()) {
112 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Hostname/IP address must be set");
114 updateStatus(ThingStatus.UNKNOWN);
115 int requestTimeout = Math.max(OpenGarageWebTargets.DEFAULT_TIMEOUT_MS, config.refresh * 1000);
116 webTargets = new OpenGarageWebTargets(config.hostname, config.port, config.password, requestTimeout);
117 this.pollScheduledFuture = this.scheduler.scheduleWithFixedDelay(this::poll, 1, config.refresh,
123 public void dispose() {
124 this.pollScheduledFuture.cancel(true);
125 this.pollScheduledFutureTransition.cancel(true);
130 * Update the state of the controller.
134 private synchronized void poll() {
136 logger.debug("Polling for state");
137 ControllerVariables controllerVariables = webTargets.getControllerVariables();
138 long lastTransitionAgoSecs = Duration.between(lastTransition, Instant.now()).getSeconds();
139 boolean inTransition = lastTransitionAgoSecs < this.config.doorTransitionTimeSeconds;
140 if (controllerVariables != null) {
141 updateStatus(ThingStatus.ONLINE);
142 updateState(OpenGarageBindingConstants.CHANNEL_OG_DISTANCE,
143 new QuantityType<>(controllerVariables.dist, MetricPrefix.CENTI(SIUnits.METRE)));
144 Function<Boolean, Boolean> maybeInvert = getInverter(
145 OpenGarageBindingConstants.CHANNEL_OG_STATUS_SWITCH);
147 if ((controllerVariables.door != 0) && (controllerVariables.door != 1)) {
148 logger.debug("Received unknown door value: {}", controllerVariables.door);
150 boolean doorOpen = controllerVariables.door == 1;
151 OnOffType onOff = maybeInvert.apply(doorOpen) ? OnOffType.ON : OnOffType.OFF;
152 UpDownType upDown = doorOpen ? UpDownType.UP : UpDownType.DOWN;
153 OpenClosedType contact = doorOpen ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
155 String transitionText;
157 transitionText = this.lastTransitionText;
159 transitionText = doorOpen ? this.config.doorOpenState : this.config.doorClosedState;
162 updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS, onOff); // deprecated channel
163 updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS_SWITCH, onOff);
165 updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS_ROLLERSHUTTER, upDown);
166 updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS_CONTACT, contact);
167 updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS_TEXT, new StringType(transitionText));
170 switch (controllerVariables.vehicle) {
172 updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE,
173 new StringType("No vehicle detected"));
176 updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE, new StringType("Vehicle detected"));
179 updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE,
180 new StringType("Vehicle status unknown"));
183 updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE,
184 new StringType("Vehicle status not available"));
188 logger.debug("Received unknown vehicle value: {}", controllerVariables.vehicle);
190 updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE_STATUS,
191 new DecimalType(controllerVariables.vehicle));
193 } catch (IOException e) {
194 logger.debug("Could not connect to OpenGarage controller", e);
195 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
196 "Could not connect to OpenGarage controller");
197 } catch (RuntimeException e) {
198 logger.debug("Unexpected error connecting to OpenGarage controller", e);
199 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
203 private void changeStatus(OpenGarageCommand status) throws OpenGarageCommunicationException {
204 webTargets.setControllerVariables(status);
207 private Function<Boolean, Boolean> getInverter(String channelUID) {
208 Channel channel = getThing().getChannel(channelUID);
209 boolean invert = channel != null && channel.getConfiguration().as(OpenGarageChannelConfiguration.class).invert;
211 return onOff -> !onOff;
213 return Function.identity();