## Supported Things
-Opengarage controllers from <https://opensprinkler.com/product/opengarage/> are supported.
+OpenGarage controllers from <https://opensprinkler.com/product/opengarage/> are supported.
## Discovery
- `port` - the port the OpenGarage is listening on. Defaults to port 80
- `refresh` - The frequency with which to refresh information from the OpenGarage controller specified in seconds. Defaults to 10 seconds.
- `password` - The password to send commands to the OpenGarage. Defaults to "opendoor"
+- `doorTransitionTimeSeconds` - Specifies how long it takes the garage door
+to fully open / close after triggering it from OpenGarage, including auditory
+beeps. Recommend to round up or pad by a second or two.
+- `doorOpeningState` - Text state to report when garage is opening. Defaults to "OPENING".
+- `doorOpenState` - Text state to report when garage is open (and not in transition). Defaults to "OPEN".
+- `doorClosingState` - Text state to report when garage is closing. Defaults to "CLOSING".
+- `doorClosedState` - Text state to report when garage is closed (and not in transition). Defaults to "CLOSED".
## Channels
|----------------------|---------------|---------------------------------------------------------------------------------------|
| distance | Number:Length | Distance reading from the OpenGarage controller (default in cm) |
| status-switch | Switch | Door status (OFF = Closed, ON = Open), set "invert=true" on channel to invert switch |
+| status-text | String | Text status of the current door state, including transition, using values from configuration: doorOpeningState, doorOpenState, doorClosingState, doorClosedState. |
| status-contact | Contact | Door status (Open or Closed) |
| status-rollershutter | Rollershutter | Door status (DOWN = Closed, UP = Open) |
| vehicle-status | Number | Report vehicle presence (0=Not Detected, 1=Detected, 2=Unknown) |
Rollershutter OpenGarage_Status_Rollershutter { channel="opengarage:opengarage:OpenGarage:status-rollershutter" }
Number:Length OpenGarage_Distance { channel="opengarage:opengarage:OpenGarage:setpoint" }
String OpenGarage_Vehicle { channel="opengarage:opengarage:OpenGarage:vehicle" }
+String OpenGarage_StatusText { channel="opengarage:opengarage:OpenGarage:status-text" }
```
opengarage.sitemap:
```perl
+Text item=OpenGarage_StatusText label="Status"
Switch item=OpenGarage_Status icon="garagedoorclosed" mappings=[ON=Open] visibility=[OpenGarage_Status == OFF]
Switch item=OpenGarage_Status icon="garagedooropen" mappings=[OFF=Close] visibility=[OpenGarage_Status == ON]
Switch item=OpenGarage_Status icon="garage"
Rollershutter item=OpenGarage_Status_Rollershutter icon="garage"
Text item=OpenGarage_Distance label="OG distance"
Text item=OpenGarage_Vehicle label="Vehicle Presence"
+
+```
+
+## Adding to HomeKit
+
+If you have the HomeKit extension installed, you can control your OpenGarage instance via your iPhone.
+To wire it up to HomeKit, you might specify the following:
+
+opengarage.items
+
```
+Group gOpenGarage "OpenGarage Door" {homekit="GarageDoorOpener"}
+Switch OpenGarage_TargetState "Target state" (gOpenGarage) {homekit="GarageDoorOpener.TargetDoorState", channel="opengarage:opengarage:deadbeef:status-switch"}
+String OpenGarage_CurrentState "Current state" (gOpenGarage) {homekit="GarageDoorOpener.CurrentDoorState", channel="opengarage:opengarage:deadbeef:status-text"}
+Switch OpenGarage_xxObstruction "Obstruction (do not use)" (gOpenGarage) {homekit="GarageDoorOpener.ObstructionStatus"}
+```
+
+The obstruction channel is not bound to any channel.
+It's needed because HomeKit requires it, and OpenGarage does not provide it.
+HomeKit requires a status for the garage door of `OPEN`, `CLOSED`, `CLOSING`, `OPENING`.
+In order to report that, we must provide state transition information.
+State transition information is inferred when the garage door state is changed.
+For `doorTransitionTimeSeconds` since the last open/close command was issued, the binding reports the state as either "closing" or "opening".
package org.openhab.binding.opengarage.internal;
import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.opengarage.internal.api.ControllerVariables;
import org.openhab.binding.opengarage.internal.api.Enums.OpenGarageCommand;
import org.openhab.core.library.types.DecimalType;
private final Logger logger = LoggerFactory.getLogger(OpenGarageHandler.class);
- private long refreshInterval;
-
private @NonNullByDefault({}) OpenGarageWebTargets webTargets;
- private @Nullable ScheduledFuture<?> pollFuture;
+
+ // reference to periodically scheduled poll task
+ private Future<?> pollScheduledFuture = CompletableFuture.completedFuture(null);
+
+ // reference to one-shot poll task which gets scheduled after a garage state change command
+ private Future<?> pollScheduledFutureTransition = CompletableFuture.completedFuture(null);
+ private Instant lastTransition;
+ private String lastTransitionText;
+
+ private OpenGarageConfiguration config = new OpenGarageConfiguration();
public OpenGarageHandler(Thing thing) {
super(thing);
+ this.lastTransition = Instant.MIN;
+ this.lastTransitionText = "";
}
@Override
- public void handleCommand(ChannelUID channelUID, Command command) {
+ public synchronized void handleCommand(ChannelUID channelUID, Command command) {
try {
logger.debug("Received command {} for thing '{}' on channel {}", command, thing.getUID().getAsString(),
channelUID.getId());
- boolean invert = isChannelInverted(channelUID.getId());
+ Function<Boolean, Boolean> maybeInvert = getInverter(channelUID.getId());
switch (channelUID.getId()) {
case OpenGarageBindingConstants.CHANNEL_OG_STATUS:
case OpenGarageBindingConstants.CHANNEL_OG_STATUS_SWITCH:
case OpenGarageBindingConstants.CHANNEL_OG_STATUS_ROLLERSHUTTER:
- if (command.equals(OnOffType.ON) || command.equals(UpDownType.UP)) {
- changeStatus(invert ? OpenGarageCommand.CLOSE : OpenGarageCommand.OPEN);
- return;
- } else if (command.equals(OnOffType.OFF) || command.equals(UpDownType.DOWN)) {
- changeStatus(invert ? OpenGarageCommand.OPEN : OpenGarageCommand.CLOSE);
- return;
- } else if (command.equals(StopMoveType.STOP) || command.equals(StopMoveType.MOVE)) {
+ if (command.equals(StopMoveType.STOP) || command.equals(StopMoveType.MOVE)) {
changeStatus(OpenGarageCommand.CLICK);
- return;
+ } else {
+ boolean doorOpen = command.equals(OnOffType.ON) || command.equals(UpDownType.UP);
+ changeStatus(maybeInvert.apply(doorOpen) ? OpenGarageCommand.OPEN : OpenGarageCommand.CLOSE);
+ this.lastTransition = Instant.now();
+ this.lastTransitionText = doorOpen ? this.config.doorOpeningState
+ : this.config.doorClosingState;
+
+ this.poll(); // invoke poll directly to communicate the door transition state
+ this.pollScheduledFutureTransition.cancel(false);
+ this.pollScheduledFutureTransition = this.scheduler.schedule(this::poll,
+ this.config.doorTransitionTimeSeconds, TimeUnit.SECONDS);
}
break;
default:
@Override
public void initialize() {
- OpenGarageConfiguration config = getConfigAs(OpenGarageConfiguration.class);
+ this.config = getConfigAs(OpenGarageConfiguration.class);
logger.debug("config.hostname = {}, refresh = {}, port = {}", config.hostname, config.refresh, config.port);
- if (config.hostname == null) {
+ if (config.hostname.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Hostname/IP address must be set");
} else {
- webTargets = new OpenGarageWebTargets(config.hostname, config.port, config.password);
- refreshInterval = config.refresh;
-
- schedulePoll();
+ updateStatus(ThingStatus.UNKNOWN);
+ int requestTimeout = Math.max(OpenGarageWebTargets.DEFAULT_TIMEOUT_MS, config.refresh * 1000);
+ webTargets = new OpenGarageWebTargets(config.hostname, config.port, config.password, requestTimeout);
+ this.pollScheduledFuture = this.scheduler.scheduleWithFixedDelay(this::poll, 1, config.refresh,
+ TimeUnit.SECONDS);
}
}
@Override
public void dispose() {
+ this.pollScheduledFuture.cancel(true);
+ this.pollScheduledFutureTransition.cancel(true);
super.dispose();
- stopPoll();
- }
-
- private void schedulePoll() {
- if (pollFuture != null) {
- pollFuture.cancel(false);
- }
- logger.debug("Scheduling poll for 1 second out, then every {} s", refreshInterval);
- pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 1, refreshInterval, TimeUnit.SECONDS);
}
- private void poll() {
+ /**
+ * Update the state of the controller.
+ *
+ *
+ */
+ private synchronized void poll() {
try {
logger.debug("Polling for state");
- pollStatus();
+ ControllerVariables controllerVariables = webTargets.getControllerVariables();
+ long lastTransitionAgoSecs = Duration.between(lastTransition, Instant.now()).getSeconds();
+ boolean inTransition = lastTransitionAgoSecs < this.config.doorTransitionTimeSeconds;
+ if (controllerVariables != null) {
+ updateStatus(ThingStatus.ONLINE);
+ updateState(OpenGarageBindingConstants.CHANNEL_OG_DISTANCE,
+ new QuantityType<>(controllerVariables.dist, MetricPrefix.CENTI(SIUnits.METRE)));
+ Function<Boolean, Boolean> maybeInvert = getInverter(
+ OpenGarageBindingConstants.CHANNEL_OG_STATUS_SWITCH);
+
+ if ((controllerVariables.door != 0) && (controllerVariables.door != 1)) {
+ logger.debug("Received unknown door value: {}", controllerVariables.door);
+ } else {
+ boolean doorOpen = controllerVariables.door == 1;
+ OnOffType onOff = maybeInvert.apply(doorOpen) ? OnOffType.ON : OnOffType.OFF;
+ UpDownType upDown = doorOpen ? UpDownType.UP : UpDownType.DOWN;
+ OpenClosedType contact = doorOpen ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
+
+ String transitionText;
+ if (inTransition) {
+ transitionText = this.lastTransitionText;
+ } else {
+ transitionText = doorOpen ? this.config.doorOpenState : this.config.doorClosedState;
+ }
+ if (!inTransition) {
+ updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS, onOff); // deprecated channel
+ updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS_SWITCH, onOff);
+ }
+ updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS_ROLLERSHUTTER, upDown);
+ updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS_CONTACT, contact);
+ updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS_TEXT, new StringType(transitionText));
+ }
+
+ switch (controllerVariables.vehicle) {
+ case 0:
+ updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE,
+ new StringType("No vehicle detected"));
+ break;
+ case 1:
+ updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE, new StringType("Vehicle detected"));
+ break;
+ case 2:
+ updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE,
+ new StringType("Vehicle status unknown"));
+ break;
+ case 3:
+ updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE,
+ new StringType("Vehicle status not available"));
+ break;
+
+ default:
+ logger.debug("Received unknown vehicle value: {}", controllerVariables.vehicle);
+ }
+ updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE_STATUS,
+ new DecimalType(controllerVariables.vehicle));
+ }
} catch (IOException e) {
logger.debug("Could not connect to OpenGarage controller", e);
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Could not connect to OpenGarage controller");
} catch (RuntimeException e) {
- logger.warn("Unexpected error connecting to OpenGarage controller", e);
+ logger.debug("Unexpected error connecting to OpenGarage controller", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
- private void stopPoll() {
- final Future<?> future = pollFuture;
- if (future != null && !future.isCancelled()) {
- future.cancel(true);
- pollFuture = null;
- }
- }
-
- private void pollStatus() throws IOException {
- ControllerVariables controllerVariables = webTargets.getControllerVariables();
- updateStatus(ThingStatus.ONLINE);
- if (controllerVariables != null) {
- updateState(OpenGarageBindingConstants.CHANNEL_OG_DISTANCE,
- new QuantityType<>(controllerVariables.dist, MetricPrefix.CENTI(SIUnits.METRE)));
- boolean invert = isChannelInverted(OpenGarageBindingConstants.CHANNEL_OG_STATUS_SWITCH);
- switch (controllerVariables.door) {
- case 0:
- updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS, invert ? OnOffType.ON : OnOffType.OFF);
- updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS_SWITCH,
- invert ? OnOffType.ON : OnOffType.OFF);
- updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS_ROLLERSHUTTER, UpDownType.DOWN);
- updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS_CONTACT, OpenClosedType.CLOSED);
- break;
- case 1:
- updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS, invert ? OnOffType.OFF : OnOffType.ON);
- updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS_SWITCH,
- invert ? OnOffType.OFF : OnOffType.ON);
- updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS_ROLLERSHUTTER, UpDownType.UP);
- updateState(OpenGarageBindingConstants.CHANNEL_OG_STATUS_CONTACT, OpenClosedType.OPEN);
- break;
- default:
- logger.warn("Received unknown door value: {}", controllerVariables.door);
- }
- switch (controllerVariables.vehicle) {
- case 0:
- updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE, new StringType("No vehicle detected"));
- break;
- case 1:
- updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE, new StringType("Vehicle detected"));
- break;
- case 2:
- updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE,
- new StringType("Vehicle status unknown"));
- break;
- case 3:
- updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE,
- new StringType("Vehicle status not available"));
- break;
- default:
- logger.warn("Received unknown vehicle value: {}", controllerVariables.vehicle);
- }
- updateState(OpenGarageBindingConstants.CHANNEL_OG_VEHICLE_STATUS,
- new DecimalType(controllerVariables.vehicle));
- }
- }
-
private void changeStatus(OpenGarageCommand status) throws OpenGarageCommunicationException {
webTargets.setControllerVariables(status);
}
- private boolean isChannelInverted(String channelUID) {
+ private Function<Boolean, Boolean> getInverter(String channelUID) {
Channel channel = getThing().getChannel(channelUID);
- return channel != null && channel.getConfiguration().as(OpenGarageChannelConfiguration.class).invert;
+ boolean invert = channel != null && channel.getConfiguration().as(OpenGarageChannelConfiguration.class).invert;
+ if (invert) {
+ return onOff -> !onOff;
+ } else {
+ return Function.identity();
+ }
}
}
<channel id="status-rollershutter" typeId="opengarage-status-rollershutter"/>
<channel id="vehicle" typeId="opengarage-vehicle"/>
<channel id="vehicle-status" typeId="opengarage-vehicle-status"/>
+ <channel id="status-text" typeId="opengarage-status-text"/>
</channels>
+ <properties>
+ <property name="thingTypeVersion">1</property>
+ </properties>
+
<config-description>
<parameter name="hostname" type="text" required="true">
<label>Hostname/IP Address</label>
<context>password</context>
<default>opendoor</default>
</parameter>
- <parameter name="refresh" type="integer">
+ <parameter name="refresh" type="integer" unit="s">
<label>Refresh Interval</label>
<description>Specifies the refresh interval in seconds.</description>
<default>60</default>
</parameter>
+ <parameter name="doorTransitionTimeSeconds" type="integer" unit="s">
+ <label>Door Transition Time</label>
+ <description>Specifies number of seconds that it takes for the garage door to fully open / close, including the time
+ it takes for OpenHab to emit beeps. Round up.</description>
+ <default>17</default>
+ </parameter>
+ <parameter name="doorOpeningState" type="text">
+ <label>Door Opening State</label>
+ <description>Text state to report when garage is opening</description>
+ <default>OPENING</default>
+ </parameter>
+ <parameter name="doorOpenState" type="text">
+ <label>Door Open State</label>
+ <description>Text state to report when garage is open (and not in transition)</description>
+ <default>OPEN</default>
+ </parameter>
+ <parameter name="doorClosingState" type="text">
+ <label>Door Closing State</label>
+ <description>Text state to report when garage is closing</description>
+ <default>CLOSING</default>
+ </parameter>
+ <parameter name="doorClosedState" type="text">
+ <label>Door Closed State</label>
+ <description>Text state to report when garage is closed (and not in transition)</description>
+ <default>CLOSED</default>
+ </parameter>
</config-description>
-
</thing-type>
<channel-type id="opengarage-distance">
</options>
</state>
</channel-type>
+ <channel-type id="opengarage-status-text">
+ <item-type>String</item-type>
+ <label>Text status</label>
+ <description>Text status of the current door state, including transition, using values from configuration:
+ doorOpeningState, doorOpenState, doorClosingState, doorClosedState.</description>
+ <state readOnly="true"/>
+ </channel-type>
</thing:thing-descriptions>