added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.opensprinkler</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,20 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons
== Third-party Content
pi4j
* License: LGPL v3.0 License
* Project: http://pi4j.com
* Source: https://github.com/Pi4J/pi4j

View File

@@ -0,0 +1,115 @@
# OpenSprinkler Binding
This binding allows allows basic control of the OpenSprinkler devices.
Stations can be controlled to be turned on or off and rain sensor state can be read.
## Supported Bridges
* HTTP (`http`) - The http bridge allows to communicate with an OpenSprinkler device through the network
## Supported Things
* OpenSprinkler Station (`station`) - to control a single station of a device, e.g. to turn it on or off
* OpenSprinkler Device (`device`) - for getting device-specific infos, e.g. if rain was detected
## Discovery
OpenSprinkler devices can be manually discovered by sending a request to every IP on the network.
Discovery needs to be run manually as this is a brute force method of finding devices that can saturate network or device available bandwidth.
## Thing Configuration
OpenSprinkler using the HTTP interface
```
Bridge opensprinkler:http:http [hostname="127.0.0.1", port=80, password="opendoor", refresh=60] {
Thing station 01 [stationIndex=1]
}
```
- hostname: Hostname or IP address of the OpenSprinkler HTTP API.
- port: Port the OpenSprinkler device is listening on. Usually 80.
- password: Admin password of the API. Factory default is: opendoor
- refresh: Number of seconds in between refreshing the Thing state with the API.
- basicUsername: (optional) Only needed when the OpenSprinkler device is behind a basic auth enforcing reverse proxy.
- basicPassword: (optional) Only needed when the OpenSprinkler device is behind a basic auth enforcing reverse proxy.
### Station Thing Configuration
The `station` thing can be used with both bridge and has the following configuration properties:
- stationIndex: The index of the station to communicate with, starting with 0 for the first station
## Channels
The following channel is supported by the `station` thing.
| Channel Type ID | Item Type | | Description |
|--------------------|-------------|----|----------------------------------------------------------|
| stationState | Switch | RW | This channel indicates whether station 01 is on or off. |
| remainingWaterTime | Number:Time | R | The time the station remains to be open. |
| nextDuration | Number:Time | RW | A configuration item, which time, if linked, will be |
| | | | used as the time the station will be kept open when |
| | | | switched on. It is advised to add persistence for items |
| | | | linked to this channel, the binding does not persist |
| | | | values of it. |
| queued | Switch | RW | Indicates that the station is queued to be turned on. |
| | | | The channel cannot be turned on, only turning it off is |
| | | | supported (which removes the station from the queue). |
When using the `nextDuration` channel, it is advised to setup persistence (e.g. MapDB) in order to persist the value through restarts.
The following is supported by the `device` thing, but only when connected using the http interface.
| Channel Type ID | Item Type | | Description |
|-----------------|------------------------|----|------------------------------------------------------------------------------------|
| rainsensor | Switch | RO | This channel indicates whether rain is detected by the device or not. |
| currentDraw | Number:ElectricCurrent | RO | Shows the current draw of the device. If the device does not have sensors |
| | | | for this metric, the channel will not be available. |
| waterlevel | Number:Dimensionless | RO | This channel shows the current water level in percent (0-250%). The water level is |
| | | | calculated based on the weather and influences the duration of the water programs. |
## Example
demo.Things:
```
Bridge opensprinkler:http:http [hostname="127.0.0.1", port=81, password="opendoor"] {
Thing station 01 [stationIndex=0]
Thing station 02 [stationIndex=1]
Thing station 03 [stationIndex=2]
Thing station 04 [stationIndex=3]
Thing station 05 [stationIndex=4]
Thing station 06 [stationIndex=5]
Thing device device
}
```
demo.items:
```
Group stations
Switch Station01 (stations) { channel="opensprinkler:station:http:01:stationState" }
Number:Time Station01RaminingTime { channel="opensprinkler:station:http:01:remainingWaterTime" }
Switch Station02 (stations) { channel="opensprinkler:station:http:02:stationState" }
Switch Station03 (stations) { channel="opensprinkler:station:http:03:stationState" }
Number:Time Station03NextDuration { channel="opensprinkler:station:http:03:nextDuration" }
Switch Station04 (stations) { channel="opensprinkler:station:http:04:stationState" }
Switch Station05 (stations) { channel="opensprinkler:station:http:05:stationState" }
Switch Station06 (stations) { channel="opensprinkler:station:http:06:stationState" }
Switch RainSensor { channel="opensprinkler:device:http:device:rainsensor" }
Number:ElectricCurrent CurrentDraw {channel="opensprinkler:device:http:device:currentDraw"}
```
demo.sitemap:
```
sitemap demo label="Main Menu"
{
Frame {
Switch item=Station01
Selection item=Station03NextDuration mappings=[300="5 min", 600="10 min"]
}
}
```

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.opensprinkler</artifactId>
<name>openHAB Add-ons :: Bundles :: OpenSprinkler Binding</name>
</project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.opensprinkler-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-opensprinkler" description="OpenSprinkler Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.opensprinkler/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link OpenSprinklerBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Chris Graham - Initial contribution
* @author Florian Schmidt - Split channels to their own things
*/
@NonNullByDefault
public class OpenSprinklerBindingConstants {
public static final String BINDING_ID = "opensprinkler";
// List of all Thing ids
public static final String HTTP_BRIDGE = "http";
public static final String PI_BRIDGE = "pi";
public static final String STATION_THING = "station";
public static final String DEVICE_THING = "device";
// List of all Thing Type UIDs
public static final ThingTypeUID OPENSPRINKLER_HTTP_BRIDGE = new ThingTypeUID(BINDING_ID, HTTP_BRIDGE);
public static final ThingTypeUID OPENSPRINKLER_STATION = new ThingTypeUID(BINDING_ID, STATION_THING);
public static final ThingTypeUID OPENSPRINKLER_DEVICE = new ThingTypeUID(BINDING_ID, DEVICE_THING);
public static final int DEFAULT_WAIT_BEFORE_INITIAL_REFRESH = 30;
public static final int DEFAULT_REFRESH_RATE = 60;
public static final int DISCOVERY_THREAD_POOL_SIZE = 15;
public static final boolean DISCOVERY_DEFAULT_AUTO_DISCOVER = false;
public static final int DISCOVERY_DEFAULT_TIMEOUT_RATE = 500;
public static final int DISCOVERY_DEFAULT_IP_TIMEOUT_RATE = 750;
// List of all Channel ids
public static final String SENSOR_RAIN = "rainsensor";
public static final String SENSOR_WATERLEVEL = "waterlevel";
public static final String SENSOR_CURRENT_DRAW = "currentDraw";
public static final String STATION_STATE = "stationState";
public static final String STATION_QUEUED = "queued";
public static final String REMAINING_WATER_TIME = "remainingWaterTime";
public static final String NEXT_DURATION = "nextDuration";
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiFactory;
import org.openhab.binding.opensprinkler.internal.handler.OpenSprinklerDeviceHandler;
import org.openhab.binding.opensprinkler.internal.handler.OpenSprinklerHttpBridgeHandler;
import org.openhab.binding.opensprinkler.internal.handler.OpenSprinklerStationHandler;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link OpenSprinklerHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Chris Graham - Initial contribution
* @author Florian Schmidt - Split channels to their own things
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.opensprinkler")
public class OpenSprinklerHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = new HashSet<>(
Arrays.asList(OPENSPRINKLER_HTTP_BRIDGE, OPENSPRINKLER_STATION, OPENSPRINKLER_DEVICE));
private OpenSprinklerApiFactory apiFactory;
@Activate
public OpenSprinklerHandlerFactory(@Reference OpenSprinklerApiFactory apiFactory) {
this.apiFactory = apiFactory;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(OPENSPRINKLER_HTTP_BRIDGE)) {
return new OpenSprinklerHttpBridgeHandler((Bridge) thing, this.apiFactory);
} else if (thingTypeUID.equals(OPENSPRINKLER_STATION)) {
return new OpenSprinklerStationHandler(thing);
} else if (thingTypeUID.equals(OPENSPRINKLER_DEVICE)) {
return new OpenSprinklerDeviceHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,128 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api;
import java.math.BigDecimal;
import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.GeneralApiException;
import org.openhab.binding.opensprinkler.internal.model.NoCurrentDrawSensorException;
import org.openhab.binding.opensprinkler.internal.model.StationProgram;
/**
* The {@link OpenSprinklerApi} interface defines the functions which are
* controllable on the OpenSprinkler API interface.
*
* @author Chris Graham - Initial contribution
*/
public interface OpenSprinklerApi {
/**
* Whether the device entered manual mode and accepts API requests to control the stations.
*
* @return True if this API interface is connected to the Open Sprinkler API. False otherwise.
*/
public abstract boolean isManualModeEnabled();
/**
* Enters the "manual" mode of the device so that API requests are accepted.
*
* @throws Exception
*/
public abstract void enterManualMode() throws CommunicationApiException;
/**
* Disables the manual mode, if it is enabled.
*
* @throws Exception
*/
public abstract void leaveManualMode() throws CommunicationApiException;
/**
* Starts a station on the OpenSprinkler device for the specified duration.
*
* @param station Index of the station to open starting at 0.
* @param duration The duration in seconds for how long the station should be turned on.
* @throws Exception
*/
public abstract void openStation(int station, BigDecimal duration)
throws CommunicationApiException, GeneralApiException;
/**
* Closes a station on the OpenSprinkler device.
*
* @param station Index of the station to open starting at 0.
* @throws Exception
*/
public abstract void closeStation(int station) throws CommunicationApiException, GeneralApiException;
/**
* Returns the state of a station on the OpenSprinkler device.
*
* @param station Index of the station to open starting at 0.
* @return True if the station is open, false if it is closed or cannot determine.
* @throws Exception
*/
public abstract boolean isStationOpen(int station) throws GeneralApiException, CommunicationApiException;
/**
* Returns the current program data of the requested station.
*
* @param station Index of the station to request data from
* @return StationProgram
* @throws Exception
*/
public abstract StationProgram retrieveProgram(int station) throws CommunicationApiException;
/**
* Returns the state of rain detection on the OpenSprinkler device.
*
* @return True if rain is detected, false if not or cannot determine.
* @throws Exception
*/
public abstract boolean isRainDetected() throws CommunicationApiException;
/**
* Returns the current draw of all connected zones of the OpenSprinkler device in milliamperes.
*
* @return current draw in milliamperes
* @throws CommunicationApiException
* @throws
*/
public abstract int currentDraw() throws CommunicationApiException, NoCurrentDrawSensorException;
/**
* Returns the water level in %.
*
* @return waterLevel in %
* @throws CommunicationApiException
* @throws
*/
public abstract int waterLevel() throws CommunicationApiException;
/**
* Returns the number of total stations that are controllable from the OpenSprinkler
* device.
*
* @return Number of stations as an int.
* @throws Exception
*/
public abstract int getNumberOfStations() throws Exception;
/**
* Returns the firmware version number.
*
* @return The firmware version of the OpenSprinkler device as an int.
* @throws Exception
*/
public abstract int getFirmwareVersion() throws CommunicationApiException;
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link OpenSprinklerApiContents} class defines common constants, which are
* used across OpenSprinkler API classes.
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class OpenSprinklerApiConstants {
public static final String HTTP_REQUEST_URL_PREFIX = "http://";
public static final String HTTPS_REQUEST_URL_PREFIX = "https://";
public static final String DEFAULT_ADMIN_PASSWORD = "opendoor";
public static final int DEFAULT_API_PORT = 80;
public static final int DEFAULT_STATION_COUNT = 8;
public static final String CMD_ENABLE_MANUAL_MODE = "mm=1";
public static final String CMD_DISABLE_MANUAL_MODE = "mm=0";
public static final String CMD_PASSWORD = "pw=";
public static final String CMD_STATION = "sid=";
public static final String CMD_STATION_ENABLE = "en=1";
public static final String CMD_STATION_DISABLE = "en=0";
public static final String CMD_STATUS_INFO = "jc";
public static final String CMD_OPTIONS_INFO = "jo";
public static final String CMD_STATION_INFO = "js";
public static final String CMD_STATION_CONTROL = "cm";
public static final String JSON_OPTION_FIRMWARE_VERSION = "fwv";
public static final String JSON_OPTION_RAINSENSOR = "rs";
public static final String JSON_OPTION_STATION = "sn";
public static final String JSON_OPTION_STATION_COUNT = "nstations";
public static final String JSON_OPTION_RESULT = "result";
}

View File

@@ -0,0 +1,84 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.GeneralApiException;
import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerHttpInterfaceConfig;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link OpenSprinklerApiFactory} class is used for creating instances of
* the OpenSprinkler API classes to interact with the OpenSprinklers HTTP or
* GPIO API's.
*
* @author Chris Graham - Initial contribution
* @author Florian Schmidt - Refactoring
*/
@Component(service = OpenSprinklerApiFactory.class)
public class OpenSprinklerApiFactory {
private @NonNull HttpClient httpClient;
@Activate
public OpenSprinklerApiFactory(@Reference HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
/**
* Factory method used to determine what version of the API is in use at the
* OpenSprinkler API and return the proper class for control of the device.
*
* @param hostname Hostname or IP address as a String of the OpenSprinkler device.
* @param port The port number the OpenSprinkler API is listening on.
* @param password Admin password for the OpenSprinkler device.
* @param basicUsername Used when basic auth is required
* @param basicPassword Used when basic auth is required
* @return OpenSprinkler HTTP API class for control of the device.
* @throws Exception
*/
public OpenSprinklerApi getHttpApi(OpenSprinklerHttpInterfaceConfig config)
throws CommunicationApiException, GeneralApiException {
int version = -1;
OpenSprinklerApi lowestSupportedApi = new OpenSprinklerHttpApiV100(this.httpClient, config);
try {
version = lowestSupportedApi.getFirmwareVersion();
} catch (CommunicationApiException exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
}
if (version >= 210 && version < 213) {
return new OpenSprinklerHttpApiV210(this.httpClient, config);
} else if (version >= 213) {
return new OpenSprinklerHttpApiV213(this.httpClient, config);
} else {
/* Need to make sure we have an older OpenSprinkler device by checking the first station. */
try {
lowestSupportedApi.isStationOpen(0);
} catch (GeneralApiException | CommunicationApiException exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: "
+ exp.getMessage());
}
return lowestSupportedApi;
}
}
}

View File

@@ -0,0 +1,382 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api;
import static org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiConstants.*;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.GeneralApiException;
import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerHttpInterfaceConfig;
import org.openhab.binding.opensprinkler.internal.model.NoCurrentDrawSensorException;
import org.openhab.binding.opensprinkler.internal.model.StationProgram;
import org.openhab.binding.opensprinkler.internal.util.Parse;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
/**
* The {@link OpenSprinklerHttpApiV100} class is used for communicating with the
* OpenSprinkler API for firmware versions less than 2.1.0
*
* @author Chris Graham - Initial contribution
* @author Florian Schmidt - Allow https URLs and basic auth
*/
class OpenSprinklerHttpApiV100 implements OpenSprinklerApi {
protected final String hostname;
protected final int port;
protected final String password;
protected final String basicUsername;
protected final String basicPassword;
protected int firmwareVersion = -1;
protected int numberOfStations = DEFAULT_STATION_COUNT;
protected boolean isInManualMode = false;
private final Gson gson = new Gson();
protected HttpRequestSender http;
/**
* Constructor for the OpenSprinkler API class to create a connection to the
* OpenSprinkler device for control and obtaining status info.
*
* @param hostname Hostname or IP address as a String of the OpenSprinkler
* device.
* @param port The port number the OpenSprinkler API is listening on.
* @param password Admin password for the OpenSprinkler device.
* @param basicUsername only needed if basic auth is required
* @param basicPassword only needed if basic auth is required
* @throws Exception
*/
OpenSprinklerHttpApiV100(final HttpClient httpClient, final OpenSprinklerHttpInterfaceConfig config)
throws GeneralApiException {
if (config.hostname == null) {
throw new GeneralApiException("The given url is null.");
}
if (config.port < 1 || config.port > 65535) {
throw new GeneralApiException("The given port is invalid.");
}
if (config.password == null) {
throw new GeneralApiException("The given password is null.");
}
if (config.hostname.startsWith(HTTP_REQUEST_URL_PREFIX)
|| config.hostname.startsWith(HTTPS_REQUEST_URL_PREFIX)) {
this.hostname = config.hostname;
} else {
this.hostname = HTTP_REQUEST_URL_PREFIX + config.hostname;
}
this.port = config.port;
this.password = config.password;
this.basicUsername = config.basicUsername;
this.basicPassword = config.basicPassword;
this.http = new HttpRequestSender(httpClient);
}
@Override
public boolean isManualModeEnabled() {
return isInManualMode;
}
@Override
public void enterManualMode() throws CommunicationApiException {
try {
http.sendHttpGet(getBaseUrl(), getRequestRequiredOptions() + "&" + CMD_ENABLE_MANUAL_MODE);
} catch (Exception exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
}
this.firmwareVersion = getFirmwareVersion();
this.numberOfStations = getNumberOfStations();
isInManualMode = true;
}
@Override
public void leaveManualMode() throws CommunicationApiException {
isInManualMode = false;
try {
http.sendHttpGet(getBaseUrl(), getRequestRequiredOptions() + "&" + CMD_DISABLE_MANUAL_MODE);
} catch (Exception exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
}
}
@Override
public void openStation(int station, BigDecimal duration) throws CommunicationApiException, GeneralApiException {
if (station < 0 || station >= numberOfStations) {
throw new GeneralApiException("This OpenSprinkler device only has " + this.numberOfStations
+ " but station " + station + " was requested to be opened.");
}
try {
http.sendHttpGet(getBaseUrl() + "sn" + station + "=1&t=" + duration, null);
} catch (Exception exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
}
}
@Override
public void closeStation(int station) throws CommunicationApiException, GeneralApiException {
if (station < 0 || station >= numberOfStations) {
throw new GeneralApiException("This OpenSprinkler device only has " + this.numberOfStations
+ " but station " + station + " was requested to be closed.");
}
http.sendHttpGet(getBaseUrl() + "sn" + station + "=0", null);
}
@Override
public boolean isStationOpen(int station) throws GeneralApiException, CommunicationApiException {
String returnContent;
if (station < 0 || station >= numberOfStations) {
throw new GeneralApiException("This OpenSprinkler device only has " + this.numberOfStations
+ " but station " + station + " was requested for a status update.");
}
try {
returnContent = http.sendHttpGet(getBaseUrl() + "sn" + station, null);
} catch (Exception exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
}
return returnContent != null && returnContent.equals("1");
}
@Override
public boolean isRainDetected() throws CommunicationApiException {
if (statusInfo().rs == 1) {
return true;
} else {
return false;
}
}
@Override
public int currentDraw() throws CommunicationApiException, NoCurrentDrawSensorException {
JcResponse info = statusInfo();
if (info.curr == null) {
throw new NoCurrentDrawSensorException();
}
return info.curr;
}
@Override
public int waterLevel() throws CommunicationApiException {
JoResponse info = getOptions();
return info.wl;
}
@Override
public int getNumberOfStations() throws CommunicationApiException {
String returnContent;
try {
returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATION_INFO, getRequestRequiredOptions());
} catch (Exception exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
}
this.numberOfStations = Parse.jsonInt(returnContent, JSON_OPTION_STATION_COUNT);
return this.numberOfStations;
}
@Override
public int getFirmwareVersion() throws CommunicationApiException {
try {
JoResponse info = getOptions();
this.firmwareVersion = info.fwv;
} catch (Exception exp) {
this.firmwareVersion = -1;
}
return this.firmwareVersion;
}
/**
* Returns the hostname and port formatted URL as a String.
*
* @return String representation of the OpenSprinkler API URL.
*/
protected String getBaseUrl() {
return hostname + ":" + port + "/";
}
/**
* Returns the required URL parameters required for every API call.
*
* @return String representation of the parameters needed during an API call.
*/
protected String getRequestRequiredOptions() {
return CMD_PASSWORD + this.password;
}
@Override
public StationProgram retrieveProgram(int station) throws CommunicationApiException {
JcResponse resp = statusInfo();
return resp.ps.stream().map(values -> new StationProgram(values.get(1))).collect(Collectors.toList())
.get(station);
}
private JcResponse statusInfo() throws CommunicationApiException {
String returnContent;
try {
returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATUS_INFO, getRequestRequiredOptions());
} catch (CommunicationApiException exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
}
JcResponse resp = gson.fromJson(returnContent, JcResponse.class);
return resp;
}
private static class JcResponse {
public List<List<Integer>> ps;
@SerializedName(value = "sn1", alternate = "rs")
public int rs;
public Integer curr;
}
private JoResponse getOptions() throws CommunicationApiException {
String returnContent;
try {
returnContent = http.sendHttpGet(getBaseUrl() + CMD_OPTIONS_INFO, getRequestRequiredOptions());
} catch (CommunicationApiException exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
}
JoResponse resp = gson.fromJson(returnContent, JoResponse.class);
return resp;
}
private static class JoResponse {
public int wl;
public int fwv;
}
/**
* This class contains helper methods for communicating HTTP GET and HTTP POST
* requests.
*
* @author Chris Graham - Initial contribution
* @author Florian Schmidt - Reduce visibility of Http communication to Api
*/
protected class HttpRequestSender {
private static final int HTTP_OK_CODE = 200;
private static final String USER_AGENT = "Mozilla/5.0";
private final HttpClient httpClient;
public HttpRequestSender(HttpClient httpClient) {
this.httpClient = httpClient;
}
/**
* Given a URL and a set parameters, send a HTTP GET request to the URL location
* created by the URL and parameters.
*
* @param url The URL to send a GET request to.
* @param urlParameters List of parameters to use in the URL for the GET
* request. Null if no parameters.
* @return String contents of the response for the GET request.
* @throws Exception
*/
public String sendHttpGet(String url, String urlParameters) throws CommunicationApiException {
String location = null;
if (urlParameters != null) {
location = url + "?" + urlParameters;
} else {
location = url;
}
ContentResponse response;
try {
response = withGeneralProperties(httpClient.newRequest(location)).method(HttpMethod.GET).send();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new CommunicationApiException("Request to OpenSprinkler device failed: " + e.getMessage());
}
if (response.getStatus() != HTTP_OK_CODE) {
throw new CommunicationApiException(
"Error sending HTTP GET request to " + url + ". Got response code: " + response.getStatus());
}
return response.getContentAsString();
}
private Request withGeneralProperties(Request request) {
request.header(HttpHeader.USER_AGENT, USER_AGENT);
if (basicUsername != null && basicPassword != null) {
String encoded = Base64.getEncoder()
.encodeToString((basicUsername + ":" + basicPassword).getBytes(StandardCharsets.UTF_8));
request.header(HttpHeader.AUTHORIZATION, "Basic " + encoded);
}
return request;
}
/**
* Given a URL and a set parameters, send a HTTP POST request to the URL
* location created by the URL and parameters.
*
* @param url The URL to send a POST request to.
* @param urlParameters List of parameters to use in the URL for the POST
* request. Null if no parameters.
* @return String contents of the response for the POST request.
* @throws Exception
*/
public String sendHttpPost(String url, String urlParameters) throws CommunicationApiException {
ContentResponse response;
try {
response = withGeneralProperties(httpClient.newRequest(url)).method(HttpMethod.POST)
.content(new StringContentProvider(urlParameters)).send();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new CommunicationApiException("Request to OpenSprinkler device failed: " + e.getMessage());
}
if (response.getStatus() != HTTP_OK_CODE) {
throw new CommunicationApiException(
"Error sending HTTP POST request to " + url + ". Got response code: " + response.getStatus());
}
return response.getContentAsString();
}
}
}

View File

@@ -0,0 +1,187 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api;
import static org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiConstants.*;
import java.math.BigDecimal;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.DataFormatErrorApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.DataMissingApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.GeneralApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.MismatchApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.NotPermittedApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.OutOfRangeApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.PageNotFoundApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.UnauthorizedApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.UnknownApiException;
import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerHttpInterfaceConfig;
import org.openhab.binding.opensprinkler.internal.util.Parse;
/**
* The {@link OpenSprinklerHttpApiV210} class is used for communicating with
* the OpenSprinkler API for firmware versions 2.1.0, 2.1.1 and 2.1.1
*
* @author Chris Graham - Initial contribution
* @author Florian Schmidt - Refactor class visibility
*/
class OpenSprinklerHttpApiV210 extends OpenSprinklerHttpApiV100 {
/**
* Constructor for the OpenSprinkler API class to create a connection to the OpenSprinkler
* device for control and obtaining status info.
*
* @param hostname Hostname or IP address as a String of the OpenSprinkler device.
* @param port The port number the OpenSprinkler API is listening on.
* @param password Admin password for the OpenSprinkler device.
* @param basicUsername only needed if basic auth is required
* @param basicPassword only needed if basic auth is required
* @throws Exception
*/
OpenSprinklerHttpApiV210(final HttpClient httpClient, final OpenSprinklerHttpInterfaceConfig config)
throws GeneralApiException {
super(httpClient, config);
}
@Override
public boolean isStationOpen(int station) throws GeneralApiException, CommunicationApiException {
String returnContent;
int stationStatus = -1;
if (station < 0 || station >= numberOfStations) {
throw new GeneralApiException("This OpenSprinkler device only has " + this.numberOfStations
+ " but station " + station + " was requested for a status update.");
}
try {
returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATION_INFO, getRequestRequiredOptions());
} catch (CommunicationApiException exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
}
try {
stationStatus = Parse.jsonIntAtArrayIndex(returnContent, JSON_OPTION_STATION, station);
} catch (Exception exp) {
throw new GeneralApiException("There was a problem parsing the station status for station " + station
+ ". Got the error: " + exp.getMessage());
}
if (stationStatus == 1) {
return true;
} else {
return false;
}
}
@Override
public void openStation(int station, BigDecimal duration) throws CommunicationApiException, GeneralApiException {
String returnContent;
if (station < 0 || station >= numberOfStations) {
throw new GeneralApiException("This OpenSprinkler device only has " + this.numberOfStations
+ " but station " + station + " was requested to be opened.");
}
try {
returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATION_CONTROL, getRequestRequiredOptions() + "&"
+ CMD_STATION + station + "&" + CMD_STATION_ENABLE + "&t=" + duration);
} catch (CommunicationApiException exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
}
resultParser(returnContent);
}
@Override
public void closeStation(int station) throws CommunicationApiException, GeneralApiException {
String returnContent;
if (station < 0 || station > numberOfStations) {
throw new GeneralApiException("This OpenSprinkler device only has " + this.numberOfStations
+ " but station " + station + " was requested to be closed.");
}
try {
returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATION_CONTROL,
getRequestRequiredOptions() + "&" + CMD_STATION + station + "&" + CMD_STATION_DISABLE);
} catch (Exception exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
}
resultParser(returnContent);
}
/**
* {@inheritDoc}
*
* @throws Exception
*/
@Override
public void enterManualMode() throws CommunicationApiException {
this.firmwareVersion = getFirmwareVersion();
this.numberOfStations = getNumberOfStations();
isInManualMode = true;
}
@Override
public void leaveManualMode() {
isInManualMode = false;
}
/**
* Creates a custom exception based on a result code from the OpenSprinkler device. This is a
* formatted response from the API as {"result: : ##}.
*
* @param returnContent String value of the return content from the OpenSprinkler device when
* an action result is returned from the API.
* @throws Exception Returns a custom exception based on the result key.
*/
protected void resultParser(String returnContent) throws GeneralApiException {
int returnCode;
try {
returnCode = Parse.jsonInt(returnContent, JSON_OPTION_RESULT);
} catch (Exception exp) {
returnCode = -1;
}
switch (returnCode) {
case -1:
throw new UnknownApiException(
"The OpenSprinkler API returnd an result that was not parseable: " + returnContent);
case 1:
return;
case 2:
throw new UnauthorizedApiException("The OpenSprinkler API returned Unauthorized response code.");
case 3:
throw new MismatchApiException("The OpenSprinkler API returned Mismatch response code.");
case 16:
throw new DataMissingApiException("The OpenSprinkler API returned Data Missing response code.");
case 17:
throw new OutOfRangeApiException("The OpenSprinkler API returned Out of Range response code.");
case 18:
throw new DataFormatErrorApiException(
"The OpenSprinkler API returned Data Format Error response code.");
case 32:
throw new PageNotFoundApiException("The OpenSprinkler API returned Page Not Found response code.");
case 48:
throw new NotPermittedApiException("The OpenSprinkler API returned Not Permitted response code.");
default:
throw new UnknownApiException("Unknown response code from OpenSprinkler API: " + returnCode);
}
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.opensprinkler.internal.api.exception.GeneralApiException;
import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerHttpInterfaceConfig;
import org.openhab.binding.opensprinkler.internal.util.Hash;
/**
* The {@link OpenSprinklerHttpApiV213} class is used for communicating with
* the OpenSprinkler API for firmware versions 2.1.3 and up.
*
* @author Chris Graham - Initial contribution
*/
class OpenSprinklerHttpApiV213 extends OpenSprinklerHttpApiV210 {
/**
* Constructor for the OpenSprinkler API class to create a connection to the OpenSprinkler
* device for control and obtaining status info.
*
* @param hostname Hostname or IP address as a String of the OpenSprinkler device.
* @param port The port number the OpenSprinkler API is listening on.
* @param password Admin password for the OpenSprinkler device.
* @param basicUsername only needed if basic auth is required
* @param basicPassword only needed if basic auth is required
* @throws Exception
*/
OpenSprinklerHttpApiV213(final HttpClient httpClient, final OpenSprinklerHttpInterfaceConfig config)
throws GeneralApiException {
super(httpClient, withHashedPassword(config));
}
private static OpenSprinklerHttpInterfaceConfig withHashedPassword(final OpenSprinklerHttpInterfaceConfig config) {
config.password = Hash.getMD5Hash(config.password);
return config;
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api.exception;
/**
* The {@link CommunicationApiException} exception is thrown when result from the OpenSprinkler
* API is problems communicating with the controller.
*
* @author Chris Graham - Initial contribution
*/
public class CommunicationApiException extends Exception {
/**
* Serial ID of this error class.
*/
private static final long serialVersionUID = 3030757939495216449L;
/**
* Basic constructor allowing the storing of a single message.
*
* @param message Descriptive message about the error.
*/
public CommunicationApiException(String message) {
super(message);
}
public CommunicationApiException(String message, Throwable e) {
super(message, e);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api.exception;
/**
* The {@link DataFormatErrorApiException} exception is thrown when result from the OpenSprinkler
* API is "result" : 18.
*
* @author Chris Graham - Initial contribution
*/
public class DataFormatErrorApiException extends GeneralApiException {
/**
* Serial ID of this error class.
*/
private static final long serialVersionUID = -6690218834582334405L;
/**
* Basic constructor allowing the storing of a single message.
*
* @param message Descriptive message about the error.
*/
public DataFormatErrorApiException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api.exception;
/**
* The {@link DataMissingApiException} exception is thrown when result from the OpenSprinkler
* API is "result" : 16.
*
* @author Chris Graham - Initial contribution
*/
public class DataMissingApiException extends GeneralApiException {
/**
* Serial ID of this error class.
*/
private static final long serialVersionUID = 5544694019051691391L;
/**
* Basic constructor allowing the storing of a single message.
*
* @param message Descriptive message about the error.
*/
public DataMissingApiException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api.exception;
/**
* The {@link GeneralApiException} exception is thrown when problems
* working with the OpenSprinkler API arise.
*
* @author Chris Graham - Initial contribution
*/
public class GeneralApiException extends Exception {
/**
* Serial ID of this error class.
*/
private static final long serialVersionUID = -2222937111494475441L;
/**
* Basic constructor allowing the storing of a single message.
*
* @param message Descriptive message about the error.
*/
public GeneralApiException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api.exception;
/**
* The {@link MismatchApiException} exception is thrown when result from the OpenSprinkler
* API is "result" : 3.
*
* @author Chris Graham - Initial contribution
*/
public class MismatchApiException extends GeneralApiException {
/**
* Serial ID of this error class.
*/
private static final long serialVersionUID = 1L;
/**
* Basic constructor allowing the storing of a single message.
*
* @param message Descriptive message about the error.
*/
public MismatchApiException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api.exception;
/**
* The {@link NotPermittedApiException} exception is thrown when result from the OpenSprinkler
* API is "result" : 48.
*
* @author Chris Graham - Initial contribution
*/
public class NotPermittedApiException extends GeneralApiException {
/**
* Serial ID of this error class.
*/
private static final long serialVersionUID = -4873603990171470019L;
/**
* Basic constructor allowing the storing of a single message.
*
* @param message Descriptive message about the error.
*/
public NotPermittedApiException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api.exception;
/**
* The {@link OutOfRangeApiException} exception is thrown when result from the OpenSprinkler
* API is "result" : 17.
*
* @author Chris Graham - Initial contribution
*/
public class OutOfRangeApiException extends GeneralApiException {
/**
* Serial ID of this error class.
*/
private static final long serialVersionUID = 928567037902289026L;
/**
* Basic constructor allowing the storing of a single message.
*
* @param message Descriptive message about the error.
*/
public OutOfRangeApiException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api.exception;
/**
* The {@link PageNotFoundApiException} exception is thrown when result from the OpenSprinkler
* API is "result" : 32.
*
* @author Chris Graham - Initial contribution
*/
public class PageNotFoundApiException extends GeneralApiException {
/**
* Serial ID of this error class.
*/
private static final long serialVersionUID = -6395534685500451473L;
/**
* Basic constructor allowing the storing of a single message.
*
* @param message Descriptive message about the error.
*/
public PageNotFoundApiException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api.exception;
/**
* The {@link UnauthorizedApiException} exception is thrown when result from the OpenSprinkler
* API is "result" : 2.
*
* @author Chris Graham - Initial contribution
*/
public class UnauthorizedApiException extends GeneralApiException {
/**
* Serial ID of this error class.
*/
private static final long serialVersionUID = 3509446091331584139L;
/**
* Basic constructor allowing the storing of a single message.
*
* @param message Descriptive message about the error.
*/
public UnauthorizedApiException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.api.exception;
/**
* The {@link UnknownApiException} exception is thrown when result from the OpenSprinkler
* API returns an unknown result.
*
* @author Chris Graham - Initial contribution
*/
public class UnknownApiException extends GeneralApiException {
/**
* Serial ID of this error class.
*/
private static final long serialVersionUID = -1166330604786956132L;
/**
* Basic constructor allowing the storing of a single message.
*
* @param message Descriptive message about the error.
*/
public UnknownApiException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.config;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.DEFAULT_REFRESH_RATE;
import static org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiConstants.*;
/**
* The {@link OpenSprinklerHttpInterfaceConfig} class defines the configuration options
* for the OpenSprinkler Thing.
*
* @author Chris Graham - Initial contribution
*/
public class OpenSprinklerHttpInterfaceConfig {
/**
* Hostname of the OpenSprinkler API.
*/
public String hostname = null;
/**
* The port the OpenSprinkler API is listening on.
*/
public int port = DEFAULT_API_PORT;
/**
* The password to connect to the OpenSprinkler API.
*/
public String password = DEFAULT_ADMIN_PASSWORD;
/**
* Number of seconds in between refreshes from the OpenSprinkler device.
*/
public int refresh = DEFAULT_REFRESH_RATE;
/**
* The basic auth username to use when the OpenSprinkler device is behind a reverse proxy with basic auth enabled.
*/
public String basicUsername = null;
/**
* The basic auth password to use when the OpenSprinkler device is behind a reverse proxy with basic auth enabled.
*/
public String basicPassword = null;
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.config;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.DEFAULT_REFRESH_RATE;
import static org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiConstants.DEFAULT_STATION_COUNT;
/**
* The {@link OpenSprinklerPiConfig} class defines the configuration options
* for the OpenSprinkler PI Thing.
*
* @author Chris Graham - Initial contribution
*/
public class OpenSprinklerPiConfig {
/**
* Number of stations to control.
*/
public int stations = DEFAULT_STATION_COUNT;
/**
* Number of seconds in between refreshes from the OpenSprinkler device.
*/
public int refresh = DEFAULT_REFRESH_RATE;
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.config;
/**
* The {@link OpenSprinklerStationConfig} class defines the configuration options
* for the OpenSprinkler Thing.
*
* @author Chris Graham - Initial contribution
*/
public class OpenSprinklerStationConfig {
/**
* The index of the station the thing is configured to control, starting with 0.
*/
public int stationIndex = -1;
}

View File

@@ -0,0 +1,104 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.discovery;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.DISCOVERY_DEFAULT_IP_TIMEOUT_RATE;
import static org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiConstants.*;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApi;
import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerHttpInterfaceConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link OpenSprinklerDiscoveryJob} class allow manual discovery of
* OpenSprinkler devices for a single IP address. This is used
* for threading to make discovery faster.
*
* @author Chris Graham - Initial contribution
*/
public class OpenSprinklerDiscoveryJob implements Runnable {
private final Logger logger = LoggerFactory.getLogger(OpenSprinklerDiscoveryJob.class);
private OpenSprinklerDiscoveryService discoveryClass;
private String ipAddress;
public OpenSprinklerDiscoveryJob(OpenSprinklerDiscoveryService service, String ip) {
this.discoveryClass = service;
this.ipAddress = ip;
}
@Override
public void run() {
if (hasOpenSprinklerDevice(this.ipAddress)) {
discoveryClass.submitDiscoveryResults(this.ipAddress);
}
}
/**
* Determines if an OpenSprinkler device is available at a given IP address.
*
* @param ip IP address of the OpenSprinkler device as a string.
* @return True if a device is found, false if not.
*/
private boolean hasOpenSprinklerDevice(String ip) {
try {
InetAddress address = InetAddress.getByName(ip);
if (canEstablishConnection(address, DEFAULT_API_PORT)) {
OpenSprinklerHttpInterfaceConfig config = new OpenSprinklerHttpInterfaceConfig();
config.hostname = ip;
config.port = DEFAULT_API_PORT;
config.password = DEFAULT_ADMIN_PASSWORD;
OpenSprinklerApi openSprinkler = discoveryClass.getApiFactory().getHttpApi(config);
return (openSprinkler != null);
} else {
logger.trace("No OpenSprinkler device found at IP address ({})", ip);
return false;
}
} catch (Exception exp) {
logger.debug("No OpenSprinkler device found at IP address ({}) because of error: {}", ip, exp.getMessage());
return false;
}
}
/**
* Tries to establish a connection to a hostname and port.
*
* @param host Hostname or IP address to connect to.
* @param port Port to attempt to connect to.
* @return True if a connection can be established, false if not.
*/
private boolean canEstablishConnection(InetAddress host, int port) {
boolean reachable = false;
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(host, port), DISCOVERY_DEFAULT_IP_TIMEOUT_RATE);
reachable = true;
} catch (IOException e) {
// do nothing
}
return reachable;
}
}

View File

@@ -0,0 +1,153 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.discovery;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.*;
import static org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiConstants.*;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.commons.net.util.SubnetUtils;
import org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiFactory;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link OpenSprinklerDiscoveryService} class allow manual discovery of
* OpenSprinkler devices.
*
* @author Chris Graham - Initial contribution
*/
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.opensprinkler")
public class OpenSprinklerDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(OpenSprinklerDiscoveryService.class);
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = new HashSet<>(
Arrays.asList(OPENSPRINKLER_HTTP_BRIDGE));
private ExecutorService discoverySearchPool;
private OpenSprinklerApiFactory apiFactory;
@Activate
public OpenSprinklerDiscoveryService(@Reference OpenSprinklerApiFactory apiFactory) {
super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_DEFAULT_TIMEOUT_RATE, DISCOVERY_DEFAULT_AUTO_DISCOVER);
this.apiFactory = apiFactory;
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return SUPPORTED_THING_TYPES_UIDS;
}
OpenSprinklerApiFactory getApiFactory() {
return this.apiFactory;
}
@Override
protected void startScan() {
logger.debug("Starting discovery of OpenSprinkler devices.");
try {
List<String> ipList = getIpAddressScanList();
discoverySearchPool = Executors.newFixedThreadPool(DISCOVERY_THREAD_POOL_SIZE);
for (String ip : ipList) {
discoverySearchPool.execute(new OpenSprinklerDiscoveryJob(this, ip));
}
discoverySearchPool.shutdown();
} catch (Exception exp) {
logger.debug("OpenSprinkler discovery service encountered an error while scanning for devices: {}",
exp.getMessage());
}
logger.debug("Completed discovery of OpenSprinkler devices.");
}
/**
* Create a new Thing with an IP address given. Uses default port and password.
*
* @param ip IP address of the OpenSprinkler device as a string.
*/
public void submitDiscoveryResults(String ip) {
ThingUID uid = new ThingUID(OPENSPRINKLER_HTTP_BRIDGE, ip.replace('.', '_'));
HashMap<String, Object> properties = new HashMap<>();
properties.put("hostname", ip);
properties.put("port", DEFAULT_API_PORT);
properties.put("password", DEFAULT_ADMIN_PASSWORD);
properties.put("refresh", DEFAULT_REFRESH_RATE);
thingDiscovered(
DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel("OpenSprinkler").build());
}
/**
* Provide a string list of all the IP addresses associated with the network interfaces on
* this machine.
*
* @return String list of IP addresses.
* @throws UnknownHostException
* @throws SocketException
*/
private List<String> getIpAddressScanList() throws UnknownHostException, SocketException {
List<String> results = new ArrayList<>();
InetAddress localHost = InetAddress.getLocalHost();
NetworkInterface networkInterface = NetworkInterface.getByInetAddress(localHost);
for (InterfaceAddress address : networkInterface.getInterfaceAddresses()) {
InetAddress ipAddress = address.getAddress();
String cidrSubnet = ipAddress.getHostAddress() + "/" + address.getNetworkPrefixLength();
/* Apache Subnet Utils only supports IP v4 for creating string list of IP's */
if (ipAddress instanceof Inet4Address) {
logger.debug("Found interface IPv4 address to scan: {}", cidrSubnet);
SubnetUtils utils = new SubnetUtils(cidrSubnet);
results.addAll(Arrays.asList(utils.getInfo().getAllAddresses()));
} else if (ipAddress instanceof Inet6Address) {
logger.debug("Found interface IPv6 address to scan: {}", cidrSubnet);
} else {
logger.debug("Found interface unknown IP type address to scan: {}", cidrSubnet);
}
}
return results;
}
}

View File

@@ -0,0 +1,105 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.handler;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.DEFAULT_WAIT_BEFORE_INITIAL_REFRESH;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApi;
import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Florian Schmidt - Refactoring
*/
@NonNullByDefault
public abstract class OpenSprinklerBaseBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(OpenSprinklerBaseBridgeHandler.class);
@Nullable
private ScheduledFuture<?> pollingJob;
@Nullable
protected OpenSprinklerApi openSprinklerDevice;
public OpenSprinklerBaseBridgeHandler(Bridge bridge) {
super(bridge);
}
public OpenSprinklerApi getApi() {
OpenSprinklerApi api = openSprinklerDevice;
if (api == null) {
throw new IllegalStateException();
}
return api;
}
@Override
public void initialize() {
pollingJob = scheduler.scheduleWithFixedDelay(this::refreshStations, DEFAULT_WAIT_BEFORE_INITIAL_REFRESH,
getRefreshInterval(), TimeUnit.SECONDS);
}
protected abstract long getRefreshInterval();
private void refreshStations() {
if (openSprinklerDevice != null) {
if (openSprinklerDevice.isManualModeEnabled()) {
updateStatus(ThingStatus.ONLINE);
this.getThing().getThings().forEach(thing -> {
OpenSprinklerBaseHandler handler = (OpenSprinklerBaseHandler) thing.getHandler();
if (handler != null) {
handler.updateChannels();
}
});
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Could not sync status with the OpenSprinkler.");
}
}
}
@Override
public void dispose() {
super.dispose();
if (openSprinklerDevice != null) {
try {
openSprinklerDevice.leaveManualMode();
} catch (CommunicationApiException e) {
logger.error("Could not close connection on teardown.", e);
}
openSprinklerDevice = null;
}
if (pollingJob != null) {
pollingJob.cancel(true);
pollingJob = null;
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Nothing to do for the bridge handler
}
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApi;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
/**
* @author Florian Schmidt - Refactoring
*/
@NonNullByDefault
public abstract class OpenSprinklerBaseHandler extends BaseThingHandler {
public OpenSprinklerBaseHandler(Thing thing) {
super(thing);
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
super.bridgeStatusChanged(bridgeStatusInfo);
if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.UNKNOWN);
}
}
@Nullable
protected OpenSprinklerApi getApi() {
Bridge bridge = getBridge();
if (bridge == null) {
return null;
}
BridgeHandler handler = bridge.getHandler();
if (!(handler instanceof OpenSprinklerBaseBridgeHandler)) {
return null;
}
try {
return ((OpenSprinklerBaseBridgeHandler) handler).getApi();
} catch (IllegalStateException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return null;
}
}
public void updateChannels() {
this.getThing().getChannels().forEach(channel -> {
updateChannel(channel.getUID());
});
if (getApi() != null) {
updateStatus(ThingStatus.ONLINE);
}
}
protected abstract void updateChannel(ChannelUID uid);
}

View File

@@ -0,0 +1,104 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.handler;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.*;
import static org.openhab.core.library.unit.MetricPrefix.MILLI;
import static org.openhab.core.library.unit.SmartHomeUnits.PERCENT;
import javax.measure.quantity.ElectricCurrent;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
import org.openhab.binding.opensprinkler.internal.model.NoCurrentDrawSensorException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Florian Schmidt - Refactoring
*/
@NonNullByDefault
public class OpenSprinklerDeviceHandler extends OpenSprinklerBaseHandler {
private final Logger logger = LoggerFactory.getLogger(OpenSprinklerDeviceHandler.class);
public OpenSprinklerDeviceHandler(Thing thing) {
super(thing);
}
@Override
protected void updateChannel(ChannelUID channel) {
try {
switch (channel.getIdWithoutGroup()) {
case SENSOR_RAIN:
if (getApi().isRainDetected()) {
updateState(channel, OnOffType.ON);
} else {
updateState(channel, OnOffType.OFF);
}
break;
case SENSOR_WATERLEVEL:
updateState(channel, QuantityType.valueOf(getApi().waterLevel(), PERCENT));
break;
case SENSOR_CURRENT_DRAW:
updateState(channel,
new QuantityType<ElectricCurrent>(getApi().currentDraw(), MILLI(SmartHomeUnits.AMPERE)));
break;
default:
logger.debug("Not updating unknown channel {}", channel);
}
} catch (CommunicationApiException | NoCurrentDrawSensorException e) {
logger.debug("Could not update {}", channel, e);
}
}
@Override
public void initialize() {
ChannelUID currentDraw = new ChannelUID(thing.getUID(), "currentDraw");
if (thing.getChannel(currentDraw) == null) {
ThingBuilder thingBuilder = editThing();
try {
getApi().currentDraw();
Channel currentDrawChannel = ChannelBuilder.create(currentDraw, "Number:ElectricCurrent")
.withType(new ChannelTypeUID(BINDING_ID, SENSOR_CURRENT_DRAW)).withLabel("Current Draw")
.withDescription("Provides the current draw.").build();
thingBuilder.withChannel(currentDrawChannel);
updateThing(thingBuilder.build());
} catch (NoCurrentDrawSensorException e) {
if (thing.getChannel(currentDraw) != null) {
thingBuilder.withoutChannel(currentDraw);
}
updateThing(thingBuilder.build());
} catch (CommunicationApiException e) {
logger.debug("Could not query current draw. Not removing channel as it could be temporary.", e);
}
}
super.initialize();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// nothing to do here
}
}

View File

@@ -0,0 +1,94 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.handler;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.DEFAULT_REFRESH_RATE;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApi;
import org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiFactory;
import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.GeneralApiException;
import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerHttpInterfaceConfig;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Florian Schmidt - Refactoring
*/
@NonNullByDefault
public class OpenSprinklerHttpBridgeHandler extends OpenSprinklerBaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(OpenSprinklerHttpBridgeHandler.class);
@Nullable
private OpenSprinklerHttpInterfaceConfig openSprinklerConfig;
private OpenSprinklerApiFactory apiFactory;
public OpenSprinklerHttpBridgeHandler(Bridge bridge, OpenSprinklerApiFactory apiFactory) {
super(bridge);
this.apiFactory = apiFactory;
}
@Override
public void initialize() {
OpenSprinklerHttpInterfaceConfig openSprinklerConfig = getConfig().as(OpenSprinklerHttpInterfaceConfig.class);
this.openSprinklerConfig = openSprinklerConfig;
logger.debug("Initializing OpenSprinkler with config (Hostname: {}, Port: {}, Refresh: {}).",
openSprinklerConfig.hostname, openSprinklerConfig.port, openSprinklerConfig.refresh);
OpenSprinklerApi openSprinklerDevice;
try {
openSprinklerDevice = apiFactory.getHttpApi(openSprinklerConfig);
this.openSprinklerDevice = openSprinklerDevice;
} catch (CommunicationApiException | GeneralApiException exp) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Could not create API connection to the OpenSprinkler device. Error received: " + exp);
return;
}
logger.debug("Successfully created API connection to the OpenSprinkler device.");
try {
openSprinklerDevice.enterManualMode();
} catch (CommunicationApiException exp) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Could not open API connection to the OpenSprinkler device. Error received: " + exp);
}
if (openSprinklerDevice.isManualModeEnabled()) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Could not initialize the connection to the OpenSprinkler.");
return;
}
super.initialize();
}
@Override
protected long getRefreshInterval() {
OpenSprinklerHttpInterfaceConfig openSprinklerConfig = this.openSprinklerConfig;
if (openSprinklerConfig == null) {
return DEFAULT_REFRESH_RATE;
}
return openSprinklerConfig.refresh;
}
}

View File

@@ -0,0 +1,234 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.handler;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.*;
import java.math.BigDecimal;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApi;
import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.GeneralApiException;
import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerStationConfig;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tec.uom.se.unit.Units;
/**
* @author Florian Schmidt - Refactoring
*/
@NonNullByDefault
public class OpenSprinklerStationHandler extends OpenSprinklerBaseHandler {
private final Logger logger = LoggerFactory.getLogger(OpenSprinklerStationHandler.class);
@Nullable
private OpenSprinklerStationConfig config;
@Nullable
private BigDecimal nextDurationTime;
public OpenSprinklerStationHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
config = getConfig().as(OpenSprinklerStationConfig.class);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
OpenSprinklerApi api = getApi();
if (api == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "OpenSprinkler bridge has no initialized API.");
return;
}
if (command != RefreshType.REFRESH) {
switch (channelUID.getIdWithoutGroup()) {
case NEXT_DURATION:
handleNextDurationCommand(channelUID, command);
break;
case STATION_STATE:
handleStationStateCommand(api, command);
break;
case STATION_QUEUED:
handleQueuedCommand(api, command);
break;
}
}
updateChannels();
}
private void handleNextDurationCommand(ChannelUID channelUID, Command command) {
if (!(command instanceof QuantityType<?>)) {
logger.info("Ignoring implausible non-QuantityType command for NEXT_DURATION");
return;
}
QuantityType<?> quantity = (QuantityType<?>) command;
this.nextDurationTime = quantity.toBigDecimal();
updateState(channelUID, quantity);
}
private void handleStationStateCommand(OpenSprinklerApi api, Command command) {
if (!(command instanceof OnOffType)) {
logger.error("Received invalid command type for OpenSprinkler station ({}).", command);
return;
}
try {
if (command == OnOffType.ON) {
api.openStation(this.getStationIndex(), nextStationDuration());
} else {
api.closeStation(this.getStationIndex());
}
} catch (CommunicationApiException | GeneralApiException exp) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Could not control the station channel " + (this.getStationIndex() + 1)
+ " for the OpenSprinkler. Error: " + exp.getMessage());
}
}
private void handleQueuedCommand(OpenSprinklerApi api, Command command) {
if (command == OnOffType.ON) {
return;
}
handleStationStateCommand(api, command);
}
private BigDecimal nextStationDuration() {
BigDecimal nextDurationItemValue = nextDurationValue();
Channel nextDuration = getThing().getChannel(NEXT_DURATION);
if (nextDuration != null && isLinked(nextDuration.getUID()) && nextDurationItemValue != null) {
return nextDurationItemValue;
}
return new BigDecimal(64800);
}
/**
* Handles determining a channel's current state from the OpenSprinkler device.
*
* @param stationId Int of the station to control. Starts at 0.
* @return State representation for the channel.
*/
@Nullable
private OnOffType getStationState(int stationId) {
boolean stationOn = false;
OpenSprinklerApi api = getApi();
if (api == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
"OpenSprinkler bridge has no initialized API.");
return null;
}
try {
stationOn = api.isStationOpen(stationId);
} catch (GeneralApiException | CommunicationApiException exp) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Could not get the station channel " + stationId
+ " current state from the OpenSprinkler thing. Error: " + exp.getMessage());
}
if (stationOn) {
return OnOffType.ON;
} else {
return OnOffType.OFF;
}
}
/**
* Handles determining a channel's current state from the OpenSprinkler device.
*
* @param stationId Int of the station to control. Starts at 0.
* @return State representation for the channel.
*/
private @Nullable QuantityType<Time> getRemainingWaterTime(int stationId) {
long remainingWaterTime = 0;
OpenSprinklerApi api = getApi();
if (api == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
"OpenSprinkler bridge has no initialized API.");
return null;
}
try {
remainingWaterTime = api.retrieveProgram(stationId).remainingWaterTime;
} catch (CommunicationApiException exp) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Could not get current state of station channel " + stationId
+ " for the OpenSprinkler device. Exception received: " + exp);
}
return new QuantityType<>(remainingWaterTime, Units.SECOND);
}
@Override
protected void updateChannel(@NonNull ChannelUID channel) {
OnOffType currentDeviceState = getStationState(this.getStationIndex());
QuantityType<Time> remainingWaterTime = getRemainingWaterTime(config.stationIndex);
switch (channel.getIdWithoutGroup()) {
case STATION_STATE:
if (currentDeviceState != null) {
updateState(channel, currentDeviceState);
}
break;
case REMAINING_WATER_TIME:
if (remainingWaterTime != null) {
updateState(channel, remainingWaterTime);
}
break;
case NEXT_DURATION:
BigDecimal duration = nextDurationValue();
if (duration != null) {
updateState(channel, new DecimalType(duration));
}
break;
case STATION_QUEUED:
if (remainingWaterTime != null && currentDeviceState != null && currentDeviceState == OnOffType.OFF
&& remainingWaterTime.intValue() != 0) {
updateState(channel, OnOffType.ON);
} else {
updateState(channel, OnOffType.OFF);
}
break;
default:
logger.debug("Not updating unknown channel {}", channel);
}
}
private @Nullable BigDecimal nextDurationValue() {
return nextDurationTime;
}
private int getStationIndex() {
OpenSprinklerStationConfig config = this.config;
if (config == null) {
throw new IllegalStateException();
}
return config.stationIndex;
}
}

View File

@@ -0,0 +1,22 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.model;
/**
* Indicates, that a device is missing a sensor to measure the current draw of itself.
*
* @author Florian Schmidt - Initial contribution
*/
public class NoCurrentDrawSensorException extends Exception {
private static final long serialVersionUID = 2251925316743442346L;
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.model;
/**
* The {@link StationProgram} class corresponds to the program set in the station.
*
* @author Florian Schmidt - Initial contribution
*/
public class StationProgram {
public final long remainingWaterTime;
public StationProgram(int remainingWaterTime) {
this.remainingWaterTime = remainingWaterTime;
}
}

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.util;
import java.security.MessageDigest;
/**
* The {@link Hash} class contains static methods for creating hashes
* of strings. Usually for password hashing.
*
* @author Chris Graham - Initial contribution
*/
public class Hash {
private static final String MD5_HASH_ALGORITHM = "MD5";
private static final String UTF8_CHAR_SET = "UTF-8";
/**
* Given a string, return the MD5 hash of the String.
*
* @param unhashed The string contents to be hashed.
* @return MD5 Hashed value of the String. Null if there is a problem hashing the String.
*/
public static String getMD5Hash(String unhashed) {
try {
byte[] bytesOfMessage = unhashed.getBytes(UTF8_CHAR_SET);
MessageDigest md5 = MessageDigest.getInstance(MD5_HASH_ALGORITHM);
byte[] hash = md5.digest(bytesOfMessage);
StringBuilder sb = new StringBuilder(2 * hash.length);
for (byte b : hash) {
sb.append(String.format("%02x", b & 0xff));
}
String digest = sb.toString();
return digest;
} catch (Exception exp) {
return null;
}
}
}

View File

@@ -0,0 +1,129 @@
/**
* Copyright (c) 2010-2020 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.opensprinkler.internal.util;
import java.util.ArrayList;
import java.util.List;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* The {@link Parse} class contains static methods for parsing JSON
* output based on key names.
*
* @author Chris Graham - Initial contribution
*/
public class Parse {
private static JsonParser jsonParser = new JsonParser();
/**
* Parses an integer from a JSON string given its key name.
*
* @param jsonData The JSON formatted string to parse from.
* @param keyName The name of the object data to return.
* @return int value of the objects data.
*/
public static int jsonInt(String jsonData, String keyName) {
JsonElement jelement = jsonParser.parse(jsonData);
JsonObject jobject = jelement.getAsJsonObject();
return jobject.get(keyName).getAsInt();
}
/**
* Parses a string from a JSON string given its key name.
*
* @param jsonData The JSON formatted string to parse from.
* @param keyName The name of the object data to return.
* @return String value of the objects data.
*/
public static String jsonString(String jsonData, String keyName) {
JsonElement jelement = jsonParser.parse(jsonData);
JsonObject jobject = jelement.getAsJsonObject();
return jobject.get(keyName).getAsString();
}
/**
* Parses an int from a JSON array given its key name in the JSON string.
*
* @param jsonData The JSON formatted string to parse from.
* @param keyName The name of the object array to search through.
* @param index Index (starting at 0) number of the item in the JSON array to return.
* @return int value of the objects data.
*/
public static int jsonIntAtArrayIndex(String jsonData, String keyName, int index) {
JsonElement jelement = jsonParser.parse(jsonData);
JsonObject jobject = jelement.getAsJsonObject();
JsonArray jarray = jobject.get(keyName).getAsJsonArray();
return jarray.get(index).getAsInt();
}
/**
* Parses a String from a JSON array given its key name in the JSON string.
*
* @param jsonData The JSON formatted string to parse from.
* @param keyName The name of the object array to search through.
* @param index Index (starting at 0) number of the item in the JSON array to return.
* @return String value of the objects data.
*/
public static String jsonStringAtArrayIndex(String jsonData, String keyName, int index) {
JsonElement jelement = jsonParser.parse(jsonData);
JsonObject jobject = jelement.getAsJsonObject();
JsonArray jarray = jobject.get(keyName).getAsJsonArray();
return jarray.get(index).getAsString();
}
/**
* Parses an int array from a JSON string given its key name.
*
* @param jsonData The JSON formatted string to parse from.
* @param keyName The name of the object array to return.
* @return List of Integers with the values of a JSON Array.
*/
public static List<Integer> jsonIntArray(String jsonData, String keyName) {
List<Integer> returnList = new ArrayList<>();
JsonElement jelement = jsonParser.parse(jsonData);
JsonObject jobject = jelement.getAsJsonObject();
JsonArray jarray = jobject.get(keyName).getAsJsonArray();
for (int i = 0; i < jarray.size(); i++) {
returnList.add(jarray.get(i).getAsInt());
}
return returnList;
}
/**
* Parses an String array from a JSON string given its key name.
*
* @param jsonData The JSON formatted string to parse from.
* @param keyName The name of the object array to search through.
* @return List of Strings with the values of a JSON Array.
*/
public static List<String> jsonStringArray(String jsonData, String keyName) {
List<String> returnList = new ArrayList<>();
JsonElement jelement = jsonParser.parse(jsonData);
JsonObject jobject = jelement.getAsJsonObject();
JsonArray jarray = jobject.get(keyName).getAsJsonArray();
for (int i = 0; i < jarray.size(); i++) {
returnList.add(jarray.get(i).getAsString());
}
return returnList;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="opensprinkler" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>OpenSprinkler Binding</name>
<description>This is the binding for OpenSprinkler.</description>
<author>Chris Graham</author>
</binding:binding>

View File

@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="opensprinkler"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="http">
<label>OpenSprinkler HTTP Interface</label>
<description>A connection to a stand alone OpenSprinkler device which communicates over HTTP.</description>
<config-description>
<parameter name="hostname" type="text">
<label>Hostname</label>
<description>The host name or IP address of the OpenSprinkler Web API interface. It may or may not start with the
protocol, e.g. in order to use https:// instead of the default http://.</description>
<default>localhost</default>
</parameter>
<parameter name="port" type="integer" min="1" max="65535">
<label>Port</label>
<description>Port of the OpenSprinkler Web API interface.</description>
<default>80</default>
</parameter>
<parameter name="password" type="text">
<label>Password</label>
<description>The admin password used to access the Web API interface.</description>
<default>opendoor</default>
</parameter>
<parameter name="refresh" type="integer">
<label>Refresh Interval</label>
<description>Specifies the refresh interval in seconds.</description>
<default>60</default>
</parameter>
<parameter name="basicUsername" type="text">
<label>Basic Auth Username</label>
<description>Used if the OpenSprinkler device is behind a basic auth protected reverse proxy.</description>
<advanced>true</advanced>
</parameter>
<parameter name="basicPassword" type="text">
<label>Basic Auth Password</label>
<description>Used if the OpenSprinkler device is behind a basic auth protected reverse proxy.</description>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>
<thing-type id="station">
<supported-bridge-type-refs>
<bridge-type-ref id="http"/>
</supported-bridge-type-refs>
<label>OpenSprinkler Station</label>
<description>Controls a station connected to the OpenSprinkler device.</description>
<channels>
<channel id="stationState" typeId="stationState"></channel>
<channel id="queued" typeId="queued"></channel>
<channel id="remainingWaterTime" typeId="remainingWaterTime"></channel>
<channel id="nextDuration" typeId="nextDuration"></channel>
</channels>
<config-description>
<parameter name="stationIndex" type="integer" required="true">
<label>Station Index</label>
<description>The index of the station, starting with 0, of the station.</description>
</parameter>
</config-description>
</thing-type>
<thing-type id="device">
<supported-bridge-type-refs>
<bridge-type-ref id="http"/>
</supported-bridge-type-refs>
<label>OpenSprinkler Device</label>
<description>A thing that receives data from the OpenSprinkler device directly.</description>
<channels>
<channel id="rainsensor" typeId="rainsensor"></channel>
<channel id="waterlevel" typeId="waterlevel"></channel>
</channels>
</thing-type>
<channel-type id="rainsensor">
<item-type>Switch</item-type>
<label>Rain</label>
<description>Provides feedback on whether the OpenSprinkler device has detected rain or not.</description>
<category>Sensor</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="waterlevel">
<item-type>Number:Dimensionless</item-type>
<label>Water Level</label>
<description>The current water level in percent</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="stationState">
<item-type>Switch</item-type>
<label>Station</label>
<description>Controls a station on the OpenSprinkler device.</description>
<category>Switch</category>
</channel-type>
<channel-type id="queued">
<item-type>Switch</item-type>
<label>Queued</label>
<description>Indicates if the station is queued to be turned on. Can be removed from the queue by turning off. ON is
read-only.</description>
<category>Switch</category>
</channel-type>
<channel-type id="remainingWaterTime">
<item-type>Number:Time</item-type>
<label>Remaining Water Time</label>
<description>Read-only property of the remaining water time of the station.</description>
<state readOnly="true" pattern="%.0f min"/>
</channel-type>
<channel-type id="nextDuration">
<item-type>Number:Time</item-type>
<label>Next Open Duration</label>
<description>The duration the station will be opened the next time it is switched on.</description>
<state readOnly="false" pattern="%.0f s"/>
</channel-type>
</thing:thing-descriptions>