From b24f3a2feb779fc1bdff1183e9c9501e80976425 Mon Sep 17 00:00:00 2001 From: Jeremy Rumpf <82414029+jeremyrumpf@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:20:42 -0400 Subject: [PATCH] [GPIO] Update GPIO binding to fix issues and provide new functionality (#13643) * [GPIO] Update the GPIO binding to fix issues and provide new functionality. * Add the ability to recover from network disconnects to pigpiod. * Provide actions to what the binding does on pigpiod connect/disconnect/reconnect. * Provide the ability to pulse outputs in a one-shot/momentary fashion. * Provide edge level configuration for gpio outputs. * Fix automated style checks. * Attempt to squash jenkins build warnings/errors. * Correct Null annotations and adjust log levels in certain areas. * Fix bracing per checkstyle review. * Normalize gpiod/pigpiod to pigpiod. openhab/openHAB normalized to OpenHAB. * Fix a copy/paste error. Output channels should not be referred as input. Attempt to clarify plurals. * Convert strings to defined constants. * Convert pulse command strings to binding constants. * Java17 instanceof pattern matching. * Nit, fix missed pulse command string to binding constant. * Add missing quotes in demo.things file definition. Workaround #11039 Fixes #11038 Fixes #13376 Signed-off-by: Jeremy Rumpf --- bundles/org.openhab.binding.gpio/README.md | 217 +++++++++-- ...ava => ChannelConfigurationException.java} | 10 +- .../gpio/internal/GPIOBindingConstants.java | 16 + .../gpio/internal/NoGpioIdException.java | 25 -- .../configuration/GPIOConfiguration.java | 3 +- .../configuration/GPIOInputConfiguration.java | 11 +- .../GPIOOutputConfiguration.java | 5 +- .../configuration/PigpioConfiguration.java | 37 ++ .../gpio/internal/handler/ChannelHandler.java | 21 +- .../handler/PigpioDigitalInputHandler.java | 177 +++++++-- .../handler/PigpioDigitalOutputHandler.java | 205 ++++++++-- .../internal/handler/PigpioRemoteHandler.java | 363 ++++++++++++++++-- .../resources/OH-INF/i18n/gpio.properties | 44 ++- .../resources/OH-INF/thing/pigpio-remote.xml | 185 ++++++++- 14 files changed, 1156 insertions(+), 163 deletions(-) rename bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/{InvalidPullUpDownException.java => ChannelConfigurationException.java} (69%) delete mode 100644 bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/NoGpioIdException.java diff --git a/bundles/org.openhab.binding.gpio/README.md b/bundles/org.openhab.binding.gpio/README.md index 863d226d20ae6..51e88383af2e0 100644 --- a/bundles/org.openhab.binding.gpio/README.md +++ b/bundles/org.openhab.binding.gpio/README.md @@ -1,19 +1,19 @@ # GPIO Binding -This binding adds GPIO support via the pigpio daemon to openhab. -It requires the pigpio () to be running on the pi that should be controlled. +This binding adds GPIO support via the pigpiod daemon to openHAB. +It requires the pigpiod daemon () to be installed on the pi that should be controlled. ## Supported Things ### pigpio-remote -This thing represents a remote pigpio instance running as daemon on a raspberry pi. +This thing represents a remote pigpiod instance running as daemon on a raspberry pi. ## Thing Configuration ### Pigpio Remote (`pigpio-remote`) -On a raspberry (or a compatible device) you have to install pigpio: +On a raspberry (or a compatible device) you have to install pigpiod. ```shell sudo apt-get install pigpiod @@ -39,71 +39,204 @@ ExecStart=/usr/bin/pigpiod sudo systemctl daemon-reload ``` -Now that Remote GPIO is enabled, get the daemon going (even if installed with apt-get): +Now that Remote GPIO is enabled, get the pigpiod daemon going (even if installed with apt-get): ```shell sudo systemctl enable pigpiod sudo systemctl start pigpiod ``` -In openHAB, set `host` to the address of the pi and the `port` to the port of pigpio (default: 8888). +## General Configuration -Note: If you are running Pigpio on same host as openHAB, then set host to **::1**. +Binding general configuration options. If you do not see all options, ensure `Show Advanced` is selected. + +### Host + +Set `Host` to the address of the Pi that pigpiod is running on. Default is 127.0.0.1 (IPV4). +Note: If you are running pigpiod on same host as openHAB, set the host to 127.0.0.1 (IPV4) or ::1 (IPV6). + +### Port + +Set `Port` to the network port that pigpiod is listening on. Default is 8888. + +### Heart Beat Interval + +The binding will poll pigpiod running on the Pi to determine if a network disconnect has occurred. +This is the interval in milliseconds that the heart beat poll occurs. Defaults to 30000 (30 seconds). + +## Input Channel Connect Action + +Input Channel Connect Action determines what happens when the binding initially connects to pigpiod. +This action only occurs once after binding startup. + +- **Do Nothing:** The default, do nothing. Input channels will retain their default value (UNDEF). +- **Refresh Channel:** Issues a refresh command on the input channels. This will refresh the channels from pigpiod causing the gpio pin state to reflect on the channel state. + +Input Channel Disconnect Connect Action: + +### Input Channel Disconnect Connect Action + +Input Channel Disconnect Connect Action determines what happens when the binding disconnects from pigpiod. + +- **Do Nothing:** The default, do nothing. Input channels will retain their current value. +- **Set Undef:** Sets the input channel states to UNDEF to indicate that pigpiod has disconnected. + +### Input Channel Reconnect Connect Action + +Input Channel Reconnect Action determines what happens when the binding reconnects to pigpiod +after a disconnect. This action does not occur on the initial binding connect to pigpiod. +startup. + +- **Do Nothing:** The default, do nothing. Input channels will retain their current value. +- **Refresh Channel:** Issues a refresh command on the input channels. This will refresh the channels from + pigpiod causing the gpio pin state to reflect on the channel state. + +### Output Channel Connect Action + +Output Channel Connect Action determines what happens when the binding initially connects to pigpiod. +This action only occurs once after binding startup. + +- **Do Nothing:** The default, do nothing. Output channels will retain their default value (UNDEF). +- **All On:** Issues a ON command to all configured output channels. +- **All Off:** Issues a OFF command to all configured output channels. +- **Refresh Channel:** Issues a refresh command on the output channels. This will refresh the channels from + pigpiod causing the gpio pin state to reflect on the channel state. NOTE: This does + not update the gpio pin state on the Pi itself. It only updates the channel state + within openHAB. + +### Output Channel Disconnect Connect Action + +Output Channel Disconnect Connect Action determines what happens when the binding disconnects from pigpiod. + +- **Do Nothing:** he default, do nothing. Input channels will retain their current value. +- **Set Undef:** Sets the output channel states to UNDEF to indicate that pigpiod has disconnected. + +### Output Channel Reconnect Connect Action + +Output Channel Reconnect Action determines what happens when the binding reconnects to pigpiod +after a disconnect. This action does not occur on the initial binding connect to pigpiod. + +- **Do Nothing:** The default, do nothing. Output channels will retain their current value. +- **Refresh Channel:** Issues a refresh command on the output channels. This will refresh the channels from + pigpiod causing the gpio pin state to reflect on the channel state. NOTE: This does + not update the gpio pin state on the Pi itself. It only updates the channel state + within openHAB. ## Channels -### Pigpio Remote +The binding has two channel types. +One for gpio input pins, and another for gpio output pins. | channel | type | description | |-----------------------|--------|---------------------------------| | pigpio-digital-input | Switch | Read-only value of the gpio pin | | pigpio-digital-output | Switch | Controls the gpio pin | -### GPIO digital input channel +### GPIO pigpio-digital-input channel configuration + +Input channels provide a read-only value of the gpio pin state using the `OnOffType` datatype. -Set the number of the pin in `gpioId`. -If you want to invert the value, set `invert` to true. -To prevent incorrect change events, you can adjust the `debouncingTime`. -Using `pullupdown` you can enable pull up or pull down resistor (OFF = Off, DOWN = Pull Down, UP = Pull Up). +GPIO Pin: -### GPIO digital output channel +The gpio pin number on the Pi that the channel will monitor. -Set the number of the pin in `gpioId`. -If you want to invert the value, set `invert` to true. +Invert: -## Full Example +Inverts the value of the gpio pin before reflecting the value on the channel. +Useful for active low gpio pins where you want the channel state to reflect an ON value when the pin is low (OFF). + +Delay Time: + +Sets a delay value in milliseconds that the gpio pin must remain at prior to updating the channel state. +This is the same as switch debouncing or hysteresis. +Default value is 10 milliseconds. +Increase this value for noisy inputs. + +Pull Up/Down Resistor: + +Sets the mode of operation for the internal pull up/ down resistor on the gpio pin. +Set this to OFF if you use external pull up/down resistors. + +Edge Detection Mode: + +Sets the mode of operation for the pin edge detection mode. +If you are not sure what the use case is for this, leave at the default value of `Either Edge`. +This is the most common mode that gpio inputs are used. + +### GPIO pigpio-digital-output channel configuration + +Output channels provide a means of controlling the output value of the gpio pin using the `OnOffType` datatype. + +GPIO Pin: + +The gpio pin number on the Pi that the channel will control. + +Invert: + +Inverts the value of the channel state before commanding the gpio pin. +Useful to simulate active low gpio pins. + +Pulse: + +Time in milliseconds that must elapse before the Pulse Command is sent to the channel. +Default value is 0, which disables the Pulse feature. + +Pulse Command: + +Together with the Pulse configuration, can be used to create a one shot or momentary output. +This is useful to simulate momentary button presses or to drive motors for a predefined amount +of time. + +- **Off:** When the ON command is issued to the channel. The Pulse feature will send an OFF command + after the Pulse duration. +- **On:** When the OFF command is issued to the channel. The Pulse feature will send an + ON command after the Pulse duration. +- **Blink:** Cycles the channel ON, OFF, ON indefinitely with a 50% duty cycle. The Blink + operation continues regardless of the commanded channel state. This was originaly + developed as a way to flash a status LED to visually confirm that a remote pigpiod + instance has connectivity to openHAB. + +## Config file example + +Example for users who still prefer configuration files. demo.things: ```java -Thing gpio:pigpio-remote:sample-pi-1 "Sample-Pi 1" [host="192.168.2.36", port=8888] { - Channels: - Type pigpio-digital-input : sample-input-1 [ gpioId=10] - Type pigpio-digital-input : sample-input-2 [ gpioId=14, invert=true] - Type pigpio-digital-output : sample-output-1 [ gpioId=3] -} - -Thing gpio:pigpio-remote:sample-pi-2 "Sample-Pi 2" [host="192.168.2.37", port=8888] { - Channels: - Type pigpio-digital-input : sample-input-3 [ gpioId=16, debouncingTime=20] - Type pigpio-digital-input : sample-input-4 [ gpioId=17, invert=true, debouncingTime=5, pullupdown="UP"] - Type pigpio-digital-output : sample-output-2 [ gpioId=4, invert=true] -} +Thing gpio:pigpio-remote:mypi "MyPi GPIO" [ host="192.168.1.5", port=8888, + heartBeatInterval=10000, + inputConnectAction="REFRESH", # REFRESH,NOTHING + inputDisconnectAction="NOTHING", # SETUNDEF,NOTHING + inputReconnectAction="REFRESH", # REFRESH,NOTHING + outputConnectAction="REFRESH", # ALLOFF,ALLON,REFRESH,NOTHING + outputDisconnectAction="SETUNDEF", # SETUNDEF,NOTHING + outputReconnectAction="REFRESH" ] # REFRESH,NOTHING + { + Channels: + Type pigpio-digital-output : BCM18 [ gpioId=18,invert=false,pulse=3000,pulseCommand="BLINK" ] # OFF,ON,BLINK + + Type pigpio-digital-output : GPO4 [ gpioId=4, invert=true,pulse=5000,pulseCommand="OFF" ] + Type pigpio-digital-output : GPO17 [ gpioId=17,invert=false,pulse=500,pulseCommand="ON" ] + Type pigpio-digital-output : GPO27 [ gpioId=27,invert=false ] + Type pigpio-digital-output : GPO22 [ gpioId=22,invert=true ] + + Type pigpio-digital-input : GPI23 [ gpioId=23,debouncingTime=50,pullupdown="UP",invert=true ] # OFF,DOWN,UP + Type pigpio-digital-input : GPI24 [ gpioId=24,debouncingTime=50,pullupdown="UP",invert=true ] + Type pigpio-digital-input : GPI25 [ gpioId=25,debouncingTime=50,pullupdown="UP",invert=true ] + Type pigpio-digital-input : GPI12 [ gpioId=12,debouncingTime=50,pullupdown="UP",invert=true ] + Type pigpio-digital-input : GPI16 [ gpioId=16,debouncingTime=50,pullupdown="UP",invert=true ] + Type pigpio-digital-input : GPI20 [ gpioId=20,debouncingTime=50,pullupdown="UP",invert=true,edgeMode="EDGE_EITHER" ] # EITHER,RISING,FALLING + Type pigpio-digital-input : GPI21 [ gpioId=21,debouncingTime=50,pullupdown="UP",invert=true,edgeMode="EDGE_RISING" ] + Type pigpio-digital-input : GPI5 [ gpioId=5, debouncingTime=50,pullupdown="UP",invert=true,edgeMode="EDGE_FALLING" ] + Type pigpio-digital-input : GPI6 [ gpioId=6, debouncingTime=50,pullupdown="UP",invert=true ] + Type pigpio-digital-input : GPI13 [ gpioId=13,debouncingTime=50,pullupdown="DOWN",invert=false ] + Type pigpio-digital-input : GPI26 [ gpioId=26,debouncingTime=50,pullupdown="OFF",invert=false ] + } ``` demo.items: ```java -Switch SampleInput1 {channel="gpio:pigpio-remote:sample-pi-1:sample-input-1"} -Switch SampleOutput1 {channel="gpio:pigpio-remote:sample-pi-1:sample-output-1"} -``` - -demo.sitemap: - -```perl -sitemap demo label="Main Menu" -{ - Switch item=SampleInput1 - Switch item=SampleOutput1 -} +Switch SampleInput1 {channel="gpio:pigpio-remote:mypi:GPI23"} +Switch SampleOutput1 {channel="gpio:pigpio-remote:mypi:GPO4"} ``` diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/InvalidPullUpDownException.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/ChannelConfigurationException.java similarity index 69% rename from bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/InvalidPullUpDownException.java rename to bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/ChannelConfigurationException.java index 76f10c332d296..1569dd3d1f686 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/InvalidPullUpDownException.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/ChannelConfigurationException.java @@ -15,11 +15,15 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * Is thrown when invalid GPIO pin Pull Up/Down resistor configuration is set + * Is thrown when a channel configuration is invalid * - * @author Martin Dagarin - Initial contribution + * @author Jeremy Rumpf - Initial contribution */ @NonNullByDefault -public class InvalidPullUpDownException extends Exception { +public class ChannelConfigurationException extends Exception { private static final long serialVersionUID = -1281107134439928767L; + + public ChannelConfigurationException(String message) { + super(message); + } } diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/GPIOBindingConstants.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/GPIOBindingConstants.java index f23e90dd00193..305ed05166bde 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/GPIOBindingConstants.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/GPIOBindingConstants.java @@ -22,6 +22,7 @@ * * @author Nils Bauer - Initial contribution * @author Martin Dagarin - Pull Up/Down GPIO pin + * @author Jeremy Rumpf - Added Action/Edge constants */ @NonNullByDefault public class GPIOBindingConstants { @@ -43,12 +44,27 @@ public class GPIOBindingConstants { public static final String DEBOUNCING_TIME = "debouncing_time"; public static final String STRICT_DEBOUNCING = "debouncing_strict"; public static final String PULLUPDOWN_RESISTOR = "pullupdown"; + public static final String ACTION_SET_UNDEF = "SETUNDEF"; + public static final String ACTION_NOTHING = "NOTHING"; + public static final String ACTION_REFRESH = "REFRESH"; + public static final String ACTION_ALL_ON = "ALLON"; + public static final String ACTION_ALL_OFF = "ALLOFF"; // Pull Up/Down modes public static final String PUD_OFF = "OFF"; public static final String PUD_DOWN = "DOWN"; public static final String PUD_UP = "UP"; + // Pulse + public static final String PULSE_OFF = "OFF"; + public static final String PULSE_ON = "ON"; + public static final String PULSE_BLINK = "BLINK"; + + // Edge modes + public static final String EDGE_EITHER = "EDGE_EITHER"; + public static final String EDGE_RISING = "EDGE_RISING"; + public static final String EDGE_FALLING = "EDGE_FALLING"; + // GPIO config properties public static final String GPIO_ID = "gpioId"; } diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/NoGpioIdException.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/NoGpioIdException.java deleted file mode 100644 index 26005f95e1133..0000000000000 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/NoGpioIdException.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.gpio.internal; - -import org.eclipse.jdt.annotation.NonNullByDefault; - -/** - * Is thrown when no gpio id is provided - * - * @author Nils Bauer - Initial contribution - */ -@NonNullByDefault -public class NoGpioIdException extends Exception { - private static final long serialVersionUID = -1281107134439928767L; -} diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOConfiguration.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOConfiguration.java index 681097d4d82f2..120d84c9c353f 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOConfiguration.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOConfiguration.java @@ -13,7 +13,6 @@ package org.openhab.binding.gpio.internal.configuration; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; /** * The {@link GPIOConfiguration} class contains fields mapping thing configuration parameters. @@ -26,7 +25,7 @@ public class GPIOConfiguration { /** * The id of the gpio pin. */ - public @Nullable Integer gpioId; + public Integer gpioId = 0; /** * Should the input/output be inverted? diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOInputConfiguration.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOInputConfiguration.java index 3121a348cfb89..abf6f6131bda1 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOInputConfiguration.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOInputConfiguration.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.gpio.internal.configuration; +import static org.openhab.binding.gpio.internal.GPIOBindingConstants.*; + import org.eclipse.jdt.annotation.NonNullByDefault; /** @@ -31,5 +33,12 @@ public class GPIOInputConfiguration extends GPIOConfiguration { * Setup a pullup resistor on the GPIO pin * OFF = PI_PUD_OFF, DOWN = PI_PUD_DOWN, UP = PI_PUD_UP */ - public String pullupdown = "OFF"; + public String pullupdown = PUD_OFF; + + /** + * Sets the input detection type. + * EDGE_EITHER = PI_EITHER_EDGE, EDGE_FALLING = PI_FALLING_EDGE, + * EDGE_RISING = PI_RISING_EDGE + */ + public String edgeMode = EDGE_EITHER; } diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOOutputConfiguration.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOOutputConfiguration.java index 750fe6e49efc0..d507364081496 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOOutputConfiguration.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOOutputConfiguration.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.gpio.internal.configuration; +import java.math.BigDecimal; + import org.eclipse.jdt.annotation.NonNullByDefault; /** @@ -21,5 +23,6 @@ */ @NonNullByDefault public class GPIOOutputConfiguration extends GPIOConfiguration { - + public BigDecimal pulse = new BigDecimal(0); + public String pulseCommand = "OFF"; } diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/PigpioConfiguration.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/PigpioConfiguration.java index 4e2ead631b76d..9acb3d22bcb0d 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/PigpioConfiguration.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/PigpioConfiguration.java @@ -32,4 +32,41 @@ public class PigpioConfiguration { * Port of pigpio on the remote raspberry pi */ public int port = 8888; + + /** + * Interval to send heartbeat checks + */ + public int heartBeatInterval = 60000; + + /** + * Input channel action on connect + * (First connect after INITIALIATION) + */ + public @Nullable String inputConnectAction; + + /** + * Input channel action on reconnect + */ + public @Nullable String inputReconnectAction; + + /** + * Input channel action on disconnect + */ + public @Nullable String inputDisconnectAction; + + /** + * Output channel action on connect + * (First connect after INITIALIATION) + */ + public @Nullable String outputConnectAction; + + /** + * Output channel action on reconnect + */ + public @Nullable String outputReconnectAction; + + /** + * Output channel action on disconnect + */ + public @Nullable String outputDisconnectAction; } diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/ChannelHandler.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/ChannelHandler.java index 3958c68857331..a87373b02486c 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/ChannelHandler.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/ChannelHandler.java @@ -13,8 +13,12 @@ package org.openhab.binding.gpio.internal.handler; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.types.Command; +import eu.xeli.jpigpio.JPigpio; +import eu.xeli.jpigpio.PigpioException; + /** * The {@link ChannelHandler} provides an interface for different pin * configuration handlers @@ -24,5 +28,20 @@ @NonNullByDefault public interface ChannelHandler { - void handleCommand(Command command); + /** + * Handles a Command being sent from the + * Openhab framework. + */ + void handleCommand(Command command) throws PigpioException; + + /** + * (Re)Establishes the JPigpio listeners. + */ + void listen(@Nullable JPigpio jPigpio) throws PigpioException; + + /** + * Terminates sending Channels status updates and + * shuts down any JPigpio listeners. + */ + void dispose(); } diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalInputHandler.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalInputHandler.java index 65086458a0071..5ecb5daa73f1f 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalInputHandler.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalInputHandler.java @@ -18,9 +18,9 @@ import java.util.function.Consumer; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.gpio.internal.ChannelConfigurationException; import org.openhab.binding.gpio.internal.GPIOBindingConstants; -import org.openhab.binding.gpio.internal.InvalidPullUpDownException; -import org.openhab.binding.gpio.internal.NoGpioIdException; import org.openhab.binding.gpio.internal.configuration.GPIOInputConfiguration; import org.openhab.core.library.types.OnOffType; import org.openhab.core.types.Command; @@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory; import eu.xeli.jpigpio.GPIO; +import eu.xeli.jpigpio.GPIOListener; import eu.xeli.jpigpio.JPigpio; import eu.xeli.jpigpio.PigpioException; @@ -39,64 +40,174 @@ * @author Nils Bauer - Initial contribution * @author Jan N. Klug - Channel redesign * @author Martin Dagarin - Pull Up/Down GPIO pin + * @author Jeremy Rumpf - Refactored for network disruptions */ @NonNullByDefault public class PigpioDigitalInputHandler implements ChannelHandler { - private final Logger logger = LoggerFactory.getLogger(PigpioDigitalInputHandler.class); private Date lastChanged = new Date(); private final GPIOInputConfiguration configuration; - private final GPIO gpio; - private final Consumer updateStatus; + private final ScheduledExecutorService scheduler; + private final Integer gpioId; + private @Nullable GPIO gpio; + private @Nullable Consumer updateStatus; + private Integer pullupdown = JPigpio.PI_PUD_OFF; + private final GPIOListener listener; + private int edgeMode = JPigpio.PI_EITHER_EDGE; - public PigpioDigitalInputHandler(GPIOInputConfiguration configuration, JPigpio jPigpio, - ScheduledExecutorService scheduler, Consumer updateStatus) - throws PigpioException, InvalidPullUpDownException, NoGpioIdException { + /** + * Constructor for PigpioDigitalOutputHandler + * + * @param configuration The channel configuration + * @param jPigpio The jPigpio instance + * @param updateStatus Is called when the state should be changed + * + * @throws PigpioException Can be thrown by Pigpio + * @throws ChannelConfigurationException Thrown on configuration error + */ + public PigpioDigitalInputHandler(GPIOInputConfiguration configuration, ScheduledExecutorService scheduler, + Consumer updateStatus) throws PigpioException, ChannelConfigurationException { this.configuration = configuration; + this.scheduler = scheduler; this.updateStatus = updateStatus; - Integer gpioId = configuration.gpioId; - if (gpioId == null) { - throw new NoGpioIdException(); + this.gpioId = configuration.gpioId; + + if (this.gpioId <= 0) { + throw new ChannelConfigurationException("Invalid gpioId value: " + this.gpioId); } - Integer pullupdown = JPigpio.PI_PUD_OFF; + String pullupdownStr = configuration.pullupdown.toUpperCase(); if (pullupdownStr.equals(GPIOBindingConstants.PUD_DOWN)) { - pullupdown = JPigpio.PI_PUD_DOWN; + this.pullupdown = JPigpio.PI_PUD_DOWN; } else if (pullupdownStr.equals(GPIOBindingConstants.PUD_UP)) { - pullupdown = JPigpio.PI_PUD_UP; + this.pullupdown = JPigpio.PI_PUD_UP; + } else if (pullupdownStr.equals(GPIOBindingConstants.PUD_OFF)) { + this.pullupdown = JPigpio.PI_PUD_OFF; } else { - if (!pullupdownStr.equals(GPIOBindingConstants.PUD_OFF)) { - throw new InvalidPullUpDownException(); - } + throw new ChannelConfigurationException("Invalid pull up/down value."); + } + + String edgeModeStr = configuration.edgeMode; + if (edgeModeStr.equals(GPIOBindingConstants.EDGE_RISING)) { + this.edgeMode = JPigpio.PI_RISING_EDGE; + } else if (edgeModeStr.equals(GPIOBindingConstants.EDGE_FALLING)) { + this.edgeMode = JPigpio.PI_FALLING_EDGE; + } else if (edgeModeStr.equals(GPIOBindingConstants.EDGE_EITHER)) { + this.edgeMode = JPigpio.PI_EITHER_EDGE; + } else { + throw new ChannelConfigurationException("Invalid edgeMode value."); } - gpio = new GPIO(jPigpio, gpioId, JPigpio.PI_INPUT); - jPigpio.gpioSetAlertFunc(gpio.getPin(), (gpio, level, tick) -> { - lastChanged = new Date(); - Date thisChange = new Date(); + + this.listener = new GPIOListener(this.gpioId, this.edgeMode) { + @Override + public void alert(int gpio, int level, long tick) { + alertFunc(gpio, level, tick); + } + }; + } + + public void alertFunc(int gpio, int level, long tick) { + this.lastChanged = new Date(); + Date thisChange = new Date(); + if (configuration.debouncingTime > 0) { scheduler.schedule(() -> afterDebounce(thisChange), configuration.debouncingTime, TimeUnit.MILLISECONDS); - }); - jPigpio.gpioSetPullUpDown(gpio.getPin(), pullupdown); + } else { + afterDebounce(thisChange); + } } + /** + * Syncronize debouncing callbacks to + * ensure they are not out of order. + */ + private Object debounceLock = new Object(); + private void afterDebounce(Date thisChange) { - try { - // Check if value changed over time - if (!thisChange.before(lastChanged)) { - updateStatus.accept(OnOffType.from(configuration.invert != gpio.getValue())); + synchronized (debounceLock) { + GPIO lgpio = this.gpio; + Consumer lupdateStatus = this.updateStatus; + + if (lgpio == null || lupdateStatus == null) { + // We raced and went offline in the meantime. + return; + } + + try { + // Check if value changed over time + if (!thisChange.before(lastChanged)) { + lupdateStatus.accept(OnOffType.from(configuration.invert != lgpio.getValue())); + } + } catch (PigpioException e) { + // -99999999 is communication related, we will let the Thing connect poll refresh it. + if (e.getErrorCode() != -99999999) { + logger.debug("Debounce exception :", e); + } } + } + } + + /** + * Establishes or re-establishes a listener on the JPigpio + * instance for the configured gpio pin. + */ + public void listen(@Nullable JPigpio jPigpio) throws PigpioException { + if (jPigpio == null) { + this.gpio = null; + return; + } + + GPIO lgpio = new GPIO(jPigpio, this.gpioId, JPigpio.PI_INPUT); + this.gpio = lgpio; + + try { + lgpio.setDirection(JPigpio.PI_INPUT); + jPigpio.gpioSetPullUpDown(lgpio.getPin(), this.pullupdown); + jPigpio.removeCallback(this.listener); } catch (PigpioException e) { - logger.warn("Unknown pigpio exception", e); + // If there is a communication error, the set alert below will throw. + if (e.getErrorCode() != -99999999) { + logger.debug("Listen exception :", e); + } } + + jPigpio.gpioSetAlertFunc(lgpio.getPin(), this.listener); } @Override - public void handleCommand(Command command) { + public void handleCommand(Command command) throws PigpioException { + GPIO lgpio = this.gpio; + Consumer lupdateStatus = this.updateStatus; + + if (lgpio == null || lupdateStatus == null) { + logger.warn("An attempt to submit a command was made when pigpiod was offline: {}", command.toString()); + return; + } + if (command instanceof RefreshType) { - try { - updateStatus.accept(OnOffType.from(configuration.invert != gpio.getValue())); - } catch (PigpioException e) { - logger.warn("Unknown pigpio exception while handling Refresh", e); + lupdateStatus.accept(OnOffType.from(configuration.invert != lgpio.getValue())); + } + } + + @Override + public void dispose() { + synchronized (debounceLock) { + GPIO lgpio = this.gpio; + + updateStatus = null; + if (lgpio != null) { + JPigpio ljPigpio = lgpio.getPigpio(); + if (ljPigpio != null) { + try { + ljPigpio.removeCallback(listener); + } catch (PigpioException e) { + // Best effort to remove listener, + // the command socket could already be dead. + if (e.getErrorCode() != -99999999) { + logger.debug("Dispose exception :", e); + } + } + } } } } diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalOutputHandler.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalOutputHandler.java index a48874c18ff95..cb1dd34bab796 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalOutputHandler.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalOutputHandler.java @@ -12,10 +12,17 @@ */ package org.openhab.binding.gpio.internal.handler; +import static org.openhab.binding.gpio.internal.GPIOBindingConstants.*; + +import java.math.BigDecimal; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.gpio.internal.NoGpioIdException; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.gpio.internal.ChannelConfigurationException; import org.openhab.binding.gpio.internal.configuration.GPIOOutputConfiguration; import org.openhab.core.library.types.OnOffType; import org.openhab.core.types.Command; @@ -33,16 +40,19 @@ * * @author Nils Bauer - Initial contribution * @author Jan N. Klug - Channel redesign + * @author Jeremy Rumpf - Refactored for network disruptions */ @NonNullByDefault public class PigpioDigitalOutputHandler implements ChannelHandler { - - /** The logger. */ private final Logger logger = LoggerFactory.getLogger(PigpioDigitalOutputHandler.class); private final GPIOOutputConfiguration configuration; - private final GPIO gpio; - private final Consumer updateStatus; + private final ScheduledExecutorService scheduler; + private final Integer gpioId; + private Integer pulseTimeout = -1; + private @Nullable String pulseCommand = ""; + private @Nullable GPIO gpio; + private @Nullable Consumer updateStatus; /** * Constructor for PigpioDigitalOutputHandler @@ -52,34 +62,183 @@ public class PigpioDigitalOutputHandler implements ChannelHandler { * @param updateStatus Is called when the state should be changed * * @throws PigpioException Can be thrown by Pigpio - * @throws NoGpioIdException Is thrown when no gpioId is defined + * @throws ChannelConfigurationException Thrown on configuration error */ - public PigpioDigitalOutputHandler(GPIOOutputConfiguration configuration, JPigpio jPigpio, - Consumer updateStatus) throws PigpioException, NoGpioIdException { + public PigpioDigitalOutputHandler(GPIOOutputConfiguration configuration, ScheduledExecutorService scheduler, + Consumer updateStatus) throws PigpioException, ChannelConfigurationException { this.configuration = configuration; + this.gpioId = configuration.gpioId; + this.scheduler = scheduler; this.updateStatus = updateStatus; - Integer gpioId = configuration.gpioId; - if (gpioId == null) { - throw new NoGpioIdException(); + + if (this.gpioId <= 0) { + throw new ChannelConfigurationException("Invalid gpioId value."); + } + + if (configuration.pulse.compareTo(BigDecimal.ZERO) > 0) { + try { + this.pulseTimeout = configuration.pulse.intValue(); + } catch (Exception e) { + throw new ChannelConfigurationException("Invalid expire value."); + } + } + + if (configuration.pulseCommand.length() > 0) { + this.pulseCommand = configuration.pulseCommand.toUpperCase(); + if (!PULSE_ON.equals(pulseCommand) && !PULSE_OFF.equals(pulseCommand) + && !PULSE_BLINK.equals(pulseCommand)) { + throw new ChannelConfigurationException("Invalid pulseCommand value."); + } } - this.gpio = new GPIO(jPigpio, gpioId, JPigpio.PI_OUTPUT); } + /** + * Future to track pulse commands. + */ + private @Nullable Future pulseJob = null; + private @Nullable OnOffType lastPulseCommand; + + /** + * Used to only keep a single gpio command handle in flight + * at a time. + */ + private Object handleLock = new Object(); + @Override - public void handleCommand(Command command) { - if (command instanceof RefreshType) { - try { - updateStatus.accept(OnOffType.from(configuration.invert != gpio.getValue())); - } catch (PigpioException e) { - logger.warn("Unknown pigpio exception while handling Refresh", e); + public void handleCommand(Command command) throws PigpioException { + synchronized (handleLock) { + GPIO lgpio = this.gpio; + Consumer lupdateStatus = this.updateStatus; + Future job = this.pulseJob; + + if (lgpio == null || lupdateStatus == null) { + logger.warn("An attempt to submit a command was made when the pigpiod was offline: {}", + command.toString()); + return; + } + + if (command instanceof RefreshType) { + lupdateStatus.accept(OnOffType.from(configuration.invert != lgpio.getValue())); + } else if (command instanceof OnOffType) { + lgpio.setValue(configuration.invert != (OnOffType.ON.equals(command))); + lupdateStatus.accept((State) command); + + if (this.pulseTimeout > 0 && this.pulseCommand != null) { + if (job != null) { + job.cancel(false); + } + + this.pulseJob = scheduler.schedule(() -> handlePulseCommand(command), this.pulseTimeout, + TimeUnit.MILLISECONDS); + } } } - if (command instanceof OnOffType) { - try { - gpio.setValue(configuration.invert != (OnOffType.ON.equals(command))); - } catch (PigpioException e) { - logger.warn("An error occured while changing the gpio value: {}", e.getMessage()); + } + + public void handlePulseCommand(@Nullable Command command) { + OnOffType eCommand = OnOffType.OFF; + + try { + synchronized (handleLock) { + GPIO lgpio = this.gpio; + Consumer lupdateStatus = this.updateStatus; + Future job = this.pulseJob; + + if (lgpio == null) { + return; + } + + if (command instanceof OnOffType) { + if (this.pulseCommand != null) { + if (PULSE_ON.equals(this.pulseCommand)) { + eCommand = OnOffType.ON; + } else if (PULSE_OFF.equals(this.pulseCommand)) { + eCommand = OnOffType.OFF; + } else if (PULSE_BLINK.equals(this.pulseCommand)) { + if (OnOffType.ON.equals(command)) { + eCommand = OnOffType.OFF; + } else if (OnOffType.OFF.equals(command)) { + eCommand = OnOffType.ON; + } + } + } else { + if (OnOffType.ON.equals(command)) { + eCommand = OnOffType.OFF; + } else if (OnOffType.OFF.equals(command)) { + eCommand = OnOffType.ON; + } + } + + logger.debug("gpio pulse command : {} {}", this.gpioId, eCommand.toString()); + + lgpio.setValue(configuration.invert != (OnOffType.ON.equals(eCommand))); + if (lupdateStatus != null) { + lupdateStatus.accept((State) eCommand); + } + + lastPulseCommand = eCommand; + + if (PULSE_BLINK.equals(this.pulseCommand) && this.pulseTimeout > 0) { + final OnOffType feCommand = eCommand; + if (job != null) { + job.cancel(false); + } + this.pulseJob = scheduler.schedule(() -> handlePulseCommand(feCommand), this.pulseTimeout, + TimeUnit.MILLISECONDS); + } + } + } + } catch (Exception e) { + logger.warn( + "Pulse command exception, {} command may not have been received by pigpiod resulting in an unknown state:", + eCommand.toString(), e); + } + } + + /** + * Configures the GPIO pin for OUTPUT. + */ + public void listen(@Nullable JPigpio jPigpio) throws PigpioException { + if (jPigpio == null) { + this.gpio = null; + return; + } + + GPIO lgpio = new GPIO(jPigpio, gpioId, JPigpio.PI_OUTPUT); + this.gpio = lgpio; + lgpio.setDirection(JPigpio.PI_OUTPUT); + scheduleBlink(); + } + + private void scheduleBlink() { + synchronized (handleLock) { + Future job = this.pulseJob; + + if (this.pulseTimeout > 0 && PULSE_BLINK.equals(configuration.pulseCommand)) { + if (job != null) { + job.cancel(false); + } + if (this.lastPulseCommand != null) { + scheduler.schedule(() -> handlePulseCommand(this.lastPulseCommand), this.pulseTimeout, + TimeUnit.MILLISECONDS); + } else { + this.pulseJob = scheduler.schedule(() -> handlePulseCommand(OnOffType.OFF), this.pulseTimeout, + TimeUnit.MILLISECONDS); + } + } + } + } + + @Override + public void dispose() { + synchronized (handleLock) { + Future job = this.pulseJob; + + if (job != null) { + job.cancel(true); } + this.updateStatus = null; + this.gpio = null; } } } diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioRemoteHandler.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioRemoteHandler.java index 3f561cd7449ac..f2ccd8fc846bb 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioRemoteHandler.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioRemoteHandler.java @@ -16,13 +16,16 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.gpio.internal.InvalidPullUpDownException; -import org.openhab.binding.gpio.internal.NoGpioIdException; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.gpio.internal.ChannelConfigurationException; import org.openhab.binding.gpio.internal.configuration.GPIOInputConfiguration; import org.openhab.binding.gpio.internal.configuration.GPIOOutputConfiguration; import org.openhab.binding.gpio.internal.configuration.PigpioConfiguration; +import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -30,6 +33,8 @@ import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,6 +49,7 @@ * * @author Nils Bauer - Initial contribution * @author Jan N. Klug - Channel redesign + * @author Jeremy Rumpf - Improve JPigpio connection handling */ @NonNullByDefault public class PigpioRemoteHandler extends BaseThingHandler { @@ -61,56 +67,355 @@ public PigpioRemoteHandler(Thing thing) { @Override public void handleCommand(ChannelUID channelUID, Command command) { - ChannelHandler channelHandler = channelHandlers.get(channelUID); - if (channelHandler != null) { - channelHandler.handleCommand(command); + try { + synchronized (this.connectionLock) { + ChannelHandler channelHandler = channelHandlers.get(channelUID); + + if (channelHandler == null || !(ThingStatus.ONLINE.equals(thing.getStatus()))) { + // We raced with connectPollWorker and lost + return; + } + + if (channelHandler instanceof PigpioDigitalInputHandler inputHandler) { + try { + inputHandler.handleCommand(command); + } catch (PigpioException pe) { + logger.warn("Input command exception on channel {} {}", channelUID, pe.toString()); + if (pe.getErrorCode() == -99999999) { + runDisconnectActions(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + pe.getLocalizedMessage()); + } + } + } else if (channelHandler instanceof PigpioDigitalOutputHandler outputHandler) { + try { + outputHandler.handleCommand(command); + } catch (PigpioException pe) { + logger.warn("Output command exception on channel {} {}", channelUID, pe.toString()); + if (pe.getErrorCode() == -99999999) { + runDisconnectActions(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + pe.getLocalizedMessage()); + } + } + } else { + logger.warn("Command received for an unknown channel: {}", channelUID); + } + } + } catch (Exception e) { + logger.warn("Command exception on channel {} {}", channelUID, e.toString()); } } + protected PigpioConfiguration config = new PigpioConfiguration(); + protected @Nullable JPigpio jPigpio = null; + @Override public void initialize() { - PigpioConfiguration config = getConfigAs(PigpioConfiguration.class); - String host = config.host; - int port = config.port; - JPigpio jPigpio; - if (host == null) { + PigpioConfiguration lconfig = getConfigAs(PigpioConfiguration.class); + this.config = lconfig; + + if (lconfig.host == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Cannot connect to PiGPIO Service on remote raspberry. IP address not set."); return; } - try { - jPigpio = new PigpioSocket(host, port); - updateStatus(ThingStatus.ONLINE); - } catch (PigpioException e) { - if (e.getErrorCode() == PigpioException.PI_BAD_SOCKET_PORT) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port out of range"); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - e.getLocalizedMessage()); - } + if (lconfig.port < 1 && lconfig.port > 65535) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Cannot connect to PiGPIO Service on remote raspberry. Invalid Port."); return; } - thing.getChannels().forEach(channel -> { + + createChannelHandlers(); + + logger.debug("gpio binding initialized"); + + connectionJob = scheduler.submit(() -> { + connectionPollWorker(); + }); + } + + protected void clearChannelHandlers() { + for (ChannelHandler handler : channelHandlers.values()) { + handler.dispose(); + } + channelHandlers.clear(); + } + + protected void createChannelHandlers() { + clearChannelHandlers(); + this.getThing().getChannels().forEach(channel -> { ChannelUID channelUID = channel.getUID(); ChannelTypeUID type = channel.getChannelTypeUID(); + try { if (CHANNEL_TYPE_DIGITAL_INPUT.equals(type)) { GPIOInputConfiguration configuration = channel.getConfiguration().as(GPIOInputConfiguration.class); - channelHandlers.put(channelUID, new PigpioDigitalInputHandler(configuration, jPigpio, scheduler, + this.channelHandlers.put(channelUID, new PigpioDigitalInputHandler(configuration, scheduler, state -> updateState(channelUID.getId(), state))); } else if (CHANNEL_TYPE_DIGITAL_OUTPUT.equals(type)) { GPIOOutputConfiguration configuration = channel.getConfiguration() .as(GPIOOutputConfiguration.class); - channelHandlers.put(channelUID, new PigpioDigitalOutputHandler(configuration, jPigpio, - state -> updateState(channelUID.getId(), state))); + PigpioDigitalOutputHandler handler = new PigpioDigitalOutputHandler(configuration, scheduler, + state -> updateState(channelUID.getId(), state)); + this.channelHandlers.put(channelUID, handler); } } catch (PigpioException e) { - logger.warn("Failed to initialize {}: {}", channelUID, e.getMessage()); - } catch (InvalidPullUpDownException e) { - logger.warn("Failed to initialize {}: Invalid Pull Up/Down resistor configuration", channelUID); - } catch (NoGpioIdException e) { - logger.warn("Failed to initialize {}: GpioId is not set", channelUID); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + String.format("Failed to initialize channel {} {}", channelUID, e.getLocalizedMessage())); + } catch (ChannelConfigurationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + String.format("Invalid configuration for channel {} {}", channelUID, e.getLocalizedMessage())); } }); + + logger.debug("gpio channels initialized"); + } + + protected void setChannelJPigpio(@Nullable JPigpio jPigpio) throws PigpioException { + if (this.channelHandlers.isEmpty()) { + createChannelHandlers(); + } + + for (ChannelHandler handler : this.channelHandlers.values()) { + handler.listen(jPigpio); + } + + logger.debug("gpio jPigpio listening"); + } + + private @Nullable Future connectionJob = null; + /** + * Syncronizes all socket related code + * to avoid racing. + */ + private Object connectionLock = new Object(); + + protected void killConnectionPoll() { + if (this.connectionJob != null) { + synchronized (this.connectionLock) { + if (this.connectionJob != null) { + Future job = this.connectionJob; + this.connectionJob = null; + if (job != null) { + logger.debug("gpio connection poll : killing"); + job.cancel(true); + } + } + } + } + } + + protected void connectionPollWorker() { + Thing thing = this.getThing(); + + synchronized (connectionLock) { + ThingStatus currentStatus = thing.getStatus(); + JPigpio ljPigpio = this.jPigpio; + + if (ThingStatus.ONLINE.equals(currentStatus) && ljPigpio != null) { + // We are ONLINE and jPigpio is instantiated, this is the normal path + try { + logger.debug("gpio connection poll : CMD_TICK"); + ljPigpio.getCurrentTick(); + } catch (PigpioException e) { + logger.debug("gpio connection poll : disconnect"); + runDisconnectActions(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + e.getLocalizedMessage()); + + // We disconnected, reschedule ourselves to try a reconnect. + // First, try a quick reconnect if the user specified a long(ish) interval + int interval = this.config.heartBeatInterval; + if (interval > 1000) { + interval = 1000; + } + + this.connectionJob = scheduler.schedule(() -> { + connectionPollWorker(); + }, interval, TimeUnit.MILLISECONDS); + + logger.warn("Pigpiod disconnected : {}", this.config.host); + + return; + } + } else { + // We are OFFLINE and jPigpio may or may not be instantiated + try { + if (ljPigpio == null) { + // First initialization or re-initialization after dispose() + // jPigpio is not up and running yet. + logger.debug("gpio connection poll : connecting"); + ljPigpio = new PigpioSocket(this.config.host, this.config.port); + this.jPigpio = ljPigpio; + setChannelJPigpio(ljPigpio); + updateStatus(ThingStatus.ONLINE); + runConnectActions(); + } else { + // jPigpio is instantiated, but not connected. + // Use it's internal reconnect logic. + logger.debug("gpio connection poll : reconnecting"); + ljPigpio.reconnect(); + // jPigpio listeners are not re-established after reconnect. + // We need to reinject them into the channel handlers. + setChannelJPigpio(ljPigpio); + updateStatus(ThingStatus.ONLINE); + runReconnectActions(); + } + + logger.debug("Pigpiod connected : {}", this.config.host); + } catch (PigpioException e) { + logger.debug("gpio connection poll : failed, {}", e.getErrorCode()); + if (currentStatus.equals(ThingStatus.ONLINE) || currentStatus.equals(ThingStatus.INITIALIZING)) { + runDisconnectActions(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + e.getLocalizedMessage()); + } + } + } + + if (this.config.heartBeatInterval > 0) { + this.connectionJob = scheduler.schedule(() -> { + connectionPollWorker(); + }, this.config.heartBeatInterval, TimeUnit.MILLISECONDS); + } else { + // User disabled periodic connections, one shot? + logger.debug("gpio connection poll : disabled"); + this.connectionJob = null; + } + } + } + + protected void runConnectActions() throws PigpioException { + if (this.config.inputConnectAction != null) { + if (ACTION_REFRESH.equals(this.config.inputConnectAction)) { + refreshInputChannels(); + } + } + + if (this.config.outputConnectAction != null) { + if (ACTION_ALL_ON.equals(this.config.outputConnectAction)) { + setOutputChannels(OnOffType.ON); + } else if (ACTION_ALL_OFF.equals(this.config.outputConnectAction)) { + setOutputChannels(OnOffType.OFF); + } else if (ACTION_REFRESH.equals(this.config.outputConnectAction)) { + refreshOutputChannels(); + } + } + } + + protected void runReconnectActions() throws PigpioException { + if (this.config.inputConnectAction != null) { + if (ACTION_REFRESH.equals(this.config.inputConnectAction)) { + refreshInputChannels(); + } + } + + if (this.config.outputConnectAction != null) { + if (ACTION_REFRESH.equals(this.config.outputConnectAction)) { + refreshOutputChannels(); + } + } + } + + protected void runDisconnectActions() { + if (this.config.inputDisconnectAction != null) { + if (ACTION_SET_UNDEF.equals(this.config.inputDisconnectAction)) { + undefInputChannels(); + } + } + + if (this.config.outputDisconnectAction != null) { + if (ACTION_SET_UNDEF.equals(this.config.outputDisconnectAction)) { + undefOutputChannels(); + } + } + } + + protected void refreshInputChannels() throws PigpioException { + logger.debug("gpio refresh input channels"); + for (ChannelUID channelUID : channelHandlers.keySet()) { + ChannelHandler handler = channelHandlers.get(channelUID); + if (handler instanceof PigpioDigitalInputHandler) { + handler.handleCommand(RefreshType.REFRESH); + postCommand(channelUID, RefreshType.REFRESH); + } + } + } + + protected void refreshOutputChannels() throws PigpioException { + logger.debug("gpio refresh output channels"); + for (ChannelUID channelUID : this.channelHandlers.keySet()) { + ChannelHandler handler = this.channelHandlers.get(channelUID); + if (handler instanceof PigpioDigitalOutputHandler) { + handler.handleCommand(RefreshType.REFRESH); + postCommand(channelUID, RefreshType.REFRESH); + } + } + } + + protected void undefInputChannels() { + logger.debug("gpio undef input channels"); + for (ChannelUID channelUID : this.channelHandlers.keySet()) { + ChannelHandler handler = this.channelHandlers.get(channelUID); + if (handler instanceof PigpioDigitalInputHandler) { + updateState(channelUID, UnDefType.UNDEF); + } + } + } + + protected void undefOutputChannels() { + logger.debug("gpio undef output channels"); + for (ChannelUID channelUID : channelHandlers.keySet()) { + ChannelHandler handler = channelHandlers.get(channelUID); + if (handler instanceof PigpioDigitalOutputHandler) { + updateState(channelUID, UnDefType.UNDEF); + } + } + } + + protected void setOutputChannels(OnOffType command) throws PigpioException { + logger.debug("gpio setting output channels: {}", command.toString()); + for (ChannelUID channelUID : this.channelHandlers.keySet()) { + ChannelHandler handler = this.channelHandlers.get(channelUID); + if (handler instanceof PigpioDigitalOutputHandler) { + handler.handleCommand(command); + postCommand(channelUID, command); + } + } + } + + @Override + public void dispose() { + try { + synchronized (this.connectionLock) { + JPigpio ljPigpio = this.jPigpio; + + killConnectionPoll(); + + if (ACTION_SET_UNDEF.equals(this.config.inputDisconnectAction)) { + undefInputChannels(); + } + if (ACTION_SET_UNDEF.equals(this.config.outputDisconnectAction)) { + undefOutputChannels(); + } + + clearChannelHandlers(); + + if (ljPigpio != null) { + try { + ljPigpio.gpioTerminate(); + this.jPigpio = null; + } catch (PigpioException e) { + // Best effort at a socket shutdown + } + } + } + logger.debug("gpio disposed"); + } catch (Exception e) { + logger.debug("Dispose exception :", e); + } + + super.dispose(); } } diff --git a/bundles/org.openhab.binding.gpio/src/main/resources/OH-INF/i18n/gpio.properties b/bundles/org.openhab.binding.gpio/src/main/resources/OH-INF/i18n/gpio.properties index 564f6dc4fac88..207beccff84f1 100644 --- a/bundles/org.openhab.binding.gpio/src/main/resources/OH-INF/i18n/gpio.properties +++ b/bundles/org.openhab.binding.gpio/src/main/resources/OH-INF/i18n/gpio.properties @@ -10,8 +10,36 @@ thing-type.gpio.pigpio-remote.description = The remote pigpio thing represents a # thing types config +thing-type.config.gpio.pigpio-remote.heartBeatInterval.label = Heart Beat Interval +thing-type.config.gpio.pigpio-remote.heartBeatInterval.description = Time in ms to send CMD_TICK calls on the communication socket. Used to detect and recover from pigpiod disconnects. thing-type.config.gpio.pigpio-remote.host.label = Network Address thing-type.config.gpio.pigpio-remote.host.description = Network address of the Raspberry Pi. +thing-type.config.gpio.pigpio-remote.inputConnectAction.label = Input Channel Connect Action +thing-type.config.gpio.pigpio-remote.inputConnectAction.description = When a pigpiod connection is first established after binding INITIALIZATION. The desired action to perform on input channels. REFRESH: Send a REFRESH command to the channel. NOTHING: Leave all channels at their current state. +thing-type.config.gpio.pigpio-remote.inputConnectAction.option.REFRESH = Refresh Channel +thing-type.config.gpio.pigpio-remote.inputConnectAction.option.NOTHING = Do Nothing +thing-type.config.gpio.pigpio-remote.inputDisconnectAction.label = Input Channel Disconnect Action +thing-type.config.gpio.pigpio-remote.inputDisconnectAction.description = When a pigpiod disconnect is encountered. The desired action to perform on input channel. SETUNDEF: Set all configured channels to UNDEF. NOTHING: Leave all channels at their current state. +thing-type.config.gpio.pigpio-remote.inputDisconnectAction.option.SETUNDEF = Set Undef +thing-type.config.gpio.pigpio-remote.inputDisconnectAction.option.NOTHING = Do Nothing +thing-type.config.gpio.pigpio-remote.inputReconnectAction.label = Input Channel Reconnect Action +thing-type.config.gpio.pigpio-remote.inputReconnectAction.description = When a pigpiod connection is re-established after being disconnected. The desired action to perform on input channels. REFRESH: Send a REFRESH command to the channel. NOTHING: Leave all channels at their current state. +thing-type.config.gpio.pigpio-remote.inputReconnectAction.option.REFRESH = Refresh Channel +thing-type.config.gpio.pigpio-remote.inputReconnectAction.option.NOTHING = Do Nothing +thing-type.config.gpio.pigpio-remote.outputConnectAction.label = Output Channel Connect Action +thing-type.config.gpio.pigpio-remote.outputConnectAction.description = When a pigpiod connection is first established after binding INITIALIZATION. The desired action to perform on outputs. ALLOFF: Update the GPIO pin to OFF. ALLON: Update the GPIO pin to ON. REFRESH: Send a REFRESH command to the channel. NOTHING: Leave all channels at their current state. +thing-type.config.gpio.pigpio-remote.outputConnectAction.option.ALLOFF = All OFF +thing-type.config.gpio.pigpio-remote.outputConnectAction.option.ALLON = All ON +thing-type.config.gpio.pigpio-remote.outputConnectAction.option.REFRESH = Refresh Channel +thing-type.config.gpio.pigpio-remote.outputConnectAction.option.NOTHING = Do Nothing +thing-type.config.gpio.pigpio-remote.outputDisconnectAction.label = Output Channel Disconnect Action +thing-type.config.gpio.pigpio-remote.outputDisconnectAction.description = When a pigpiod disconnect is encountered. The desired action to perform on outputs. SETUNDEF: Set all configured channels to UNDEF. NOTHING: Leave all channels at their current state. +thing-type.config.gpio.pigpio-remote.outputDisconnectAction.option.SETUNDEF = Set Undef +thing-type.config.gpio.pigpio-remote.outputDisconnectAction.option.NOTHING = Do Nothing +thing-type.config.gpio.pigpio-remote.outputReconnectAction.label = Output Channel Reconnect Action +thing-type.config.gpio.pigpio-remote.outputReconnectAction.description = When a pigpiod connection is re-established after being disconnected. The desired action to perform on outputs. REFRESH: Send a REFRESH command to the channel. NOTHING: Leave all channels at their current state. +thing-type.config.gpio.pigpio-remote.outputReconnectAction.option.REFRESH = Refresh Channel +thing-type.config.gpio.pigpio-remote.outputReconnectAction.option.NOTHING = Do Nothing thing-type.config.gpio.pigpio-remote.port.label = Port thing-type.config.gpio.pigpio-remote.port.description = Port of pigpio on the remote Raspberry Pi. @@ -25,10 +53,16 @@ channel-type.gpio.pigpio-digital-output.description = Set digital state of a GPI # channel types config channel-type.config.gpio.pigpio-digital-input.debouncingTime.label = Delay Time -channel-type.config.gpio.pigpio-digital-input.debouncingTime.description = Time in ms to double check if value hasn't changed +channel-type.config.gpio.pigpio-digital-input.debouncingTime.description = Time in ms to double check if value hasn't changed. Be sure that the maximum latency of your network is not greater than two times this value. +channel-type.config.gpio.pigpio-digital-input.edgeMode.label = Edge Detection Mode +channel-type.config.gpio.pigpio-digital-input.edgeMode.description = Edge detection mode of the GPIO pin +channel-type.config.gpio.pigpio-digital-input.edgeMode.option.EDGE_EITHER = Either Edge +channel-type.config.gpio.pigpio-digital-input.edgeMode.option.EDGE_FALLING = Falling Edge +channel-type.config.gpio.pigpio-digital-input.edgeMode.option.EDGE_RISING = Rising Edge channel-type.config.gpio.pigpio-digital-input.gpioId.label = GPIO Pin channel-type.config.gpio.pigpio-digital-input.gpioId.description = GPIO pin to use as input channel-type.config.gpio.pigpio-digital-input.invert.label = Invert +channel-type.config.gpio.pigpio-digital-input.invert.description = Inverts the GPIO pin state from the channel state. Setting this to true can simulate an active low GPIO pin. channel-type.config.gpio.pigpio-digital-input.pullupdown.label = Pull Up/Down Resistor channel-type.config.gpio.pigpio-digital-input.pullupdown.description = Configure Pull Up/Down Resistor of GPIO pin channel-type.config.gpio.pigpio-digital-input.pullupdown.option.OFF = Off @@ -37,3 +71,11 @@ channel-type.config.gpio.pigpio-digital-input.pullupdown.option.UP = Pull Up channel-type.config.gpio.pigpio-digital-output.gpioId.label = GPIO Pin channel-type.config.gpio.pigpio-digital-output.gpioId.description = GPIO pin to use as output channel-type.config.gpio.pigpio-digital-output.invert.label = Invert +channel-type.config.gpio.pigpio-digital-output.invert.description = Inverts the GPIO pin state from the channel state. Setting this to true can simulate an active low GPIO pin. +channel-type.config.gpio.pigpio-digital-output.pulse.label = Pulse +channel-type.config.gpio.pigpio-digital-output.pulse.description = Issues the pulse command after the given number of milliseconds. Used to pulse outputs. +channel-type.config.gpio.pigpio-digital-output.pulseCommand.label = Pulse Command +channel-type.config.gpio.pigpio-digital-output.pulseCommand.description = The command to issue after the pulse duration to complete the pulse. Blink will alternate ON/OFF, useful for beacons or flashing leds. +channel-type.config.gpio.pigpio-digital-output.pulseCommand.option.OFF = Off +channel-type.config.gpio.pigpio-digital-output.pulseCommand.option.ON = On +channel-type.config.gpio.pigpio-digital-output.pulseCommand.option.BLINK = Blink diff --git a/bundles/org.openhab.binding.gpio/src/main/resources/OH-INF/thing/pigpio-remote.xml b/bundles/org.openhab.binding.gpio/src/main/resources/OH-INF/thing/pigpio-remote.xml index f71d13ca66653..8e17e0e559add 100644 --- a/bundles/org.openhab.binding.gpio/src/main/resources/OH-INF/thing/pigpio-remote.xml +++ b/bundles/org.openhab.binding.gpio/src/main/resources/OH-INF/thing/pigpio-remote.xml @@ -18,6 +18,7 @@ network_address Network address of the Raspberry Pi. + 127.0.0.1 port @@ -25,6 +26,141 @@ Port of pigpio on the remote Raspberry Pi. 8888 + + time + + + Time in ms to send CMD_TICK calls on the communication socket. + Used to detect and recover from pigpiod + disconnects. + + 30000 + true + + + + + When a pigpiod connection is first established after + binding INITIALIZATION. + The desired action to + perform on input channels. + REFRESH: Send a REFRESH command + to the channel. + NOTHING: Leave + all channels + at their + current + state. + + + + + + true + NOTHING + true + + + + + When a pigpiod disconnect is encountered. + The desired action to perform on input channel. + SETUNDEF: Set + all configured channels to UNDEF. + NOTHING: Leave all channels at their current state. + + + + + + true + NOTHING + true + + + + + When a pigpiod connection is re-established after being disconnected. + The desired action to perform on + input channels. + REFRESH: Send a REFRESH command + to the channel. + NOTHING: Leave all + channels at their + current + state. + + + + + + true + NOTHING + true + + + + + When a pigpiod connection is first established after + binding INITIALIZATION. + The desired action to + perform on outputs. + ALLOFF: Update the GPIO pin to OFF. + ALLON: Update the GPIO pin to ON. + REFRESH: Send a REFRESH + command + to the channel. + NOTHING: Leave all + channels at their current + state. + + + + + + + + true + NOTHING + true + + + + + When a pigpiod disconnect is encountered. + The desired action to perform on outputs. + SETUNDEF: Set all + configured channels to UNDEF. + NOTHING: Leave all channels at their current state. + + + + + + true + NOTHING + true + + + + + When a pigpiod connection is re-established after being disconnected. + The desired action to perform on + outputs. + REFRESH: Send a REFRESH command + to the channel. + NOTHING: Leave all + channels at their current + state. + + + + + + true + NOTHING + true + @@ -35,18 +171,28 @@ - + GPIO pin to use as input false + + Inverts the GPIO pin state from the channel state. + Setting this to true can simulate an active low GPIO + pin. + time - Time in ms to double check if value hasn't changed + + Time in ms to double check if value hasn't changed. + Be sure that the maximum latency of your + network is + not greater than two times this value. + 10 true @@ -61,6 +207,17 @@ true OFF + + + Edge detection mode of the GPIO pin + + + + + + true + EDGE_EITHER + @@ -76,6 +233,30 @@ false + + Inverts the GPIO pin state from the channel state. + Setting this to true can simulate an active low GPIO + pin. + + + + + Issues the pulse command after the given number of milliseconds. Used to pulse outputs. + 0 + + + + The command to issue after the pulse duration to complete the pulse. Blink will alternate ON/OFF, + useful for beacons + or + flashing leds. + + + + + + true + OFF