[opensprinkler] Improvements: Remove apache.commons, fix bugs and warnings (#9869)

* Fix: current channel would not get added.

Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Fix: online and offline now detect correctly.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Improve discovery.

Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Bug fixes.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Misc Improvements and log cleanup.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Make current sensor channel dynamically get removed if not supported.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* fix compiler warning.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* readme updates.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Shift refresh()


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Add RSSI and water flow meter count channels.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Change to flowSensorCount


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Fix bug


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Fix NPE on parsing jsReplies.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Add new programs channel.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Fix wrong api use.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Change to using max time if the time is null.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Add device withRepresentationProperty

Signed-off-by: Matthew Skinner <matt@pcmus.com>

* update readme


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Fix rain sensor not working in 219 firmware.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Spotless fixes.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* fix gson double up error.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Add more channels


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* refresh completed before bridge goes online.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Simplify.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Allow non default password to not stop discovery.

Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Add ignoresRain channel


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Simplify commands in station handler.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Add new rainDelay channel.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Make constants consistent.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Add categories.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Changes requested by FlorianSW

Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Stop catching and throwing new exp.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Create a state class.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.opensprinkler/src/main/java/org/openhab/binding/opensprinkler/internal/discovery/OpenSprinklerDiscoveryService.java

Signed-off-by: Matthew Skinner <matt@pcmus.com>

Co-authored-by: Connor Petty <mistercpp2000@gmail.com>

* Fix for EOF exception that stops polling with bad wifi.

Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Change signal strength over to 0-4 range


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Handle toUnit null returns.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Support discovery on subnets that are not /24


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* update readme for recent changes to signalStrength channel.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Fix code anal issue and readme.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Remove info logging.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* remove traces of old dependancy.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Changes for fwolter


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* support RSSI and rssi


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Update after new command sent.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* improve delayedRefresh


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Remove supressWarnings


Signed-off-by: Matthew Skinner <matt@pcmus.com>

Co-authored-by: Connor Petty <mistercpp2000@gmail.com>
This commit is contained in:
Matthew Skinner
2021-07-11 17:40:29 +10:00
committed by GitHub
parent 1e8be24fce
commit 6fc24e4aa4
40 changed files with 1313 additions and 767 deletions

View File

@@ -1,10 +1,8 @@
<?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>
<feature dependency="true">openhab.tp-commons-net</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.opensprinkler/${project.version}</bundle>
</feature>
</features>

View File

@@ -12,6 +12,8 @@
*/
package org.openhab.binding.opensprinkler.internal;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
@@ -25,6 +27,32 @@ import org.openhab.core.thing.ThingTypeUID;
@NonNullByDefault
public class OpenSprinklerBindingConstants {
public static final String BINDING_ID = "opensprinkler";
public static final String DEFAULT_ADMIN_PASSWORD = "opendoor";
public static final int DEFAULT_STATION_COUNT = 8;
public static final String HTTP_REQUEST_URL_PREFIX = "http://";
public static final String HTTPS_REQUEST_URL_PREFIX = "https://";
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_PROGRAM_DATA = "jp";
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";
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;
public static final BigDecimal MAX_TIME_SECONDS = new BigDecimal(64800);
// List of all Thing ids
public static final String HTTP_BRIDGE = "http";
@@ -37,19 +65,21 @@ public class OpenSprinklerBindingConstants {
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_SIGNAL_STRENGTH = "signalStrength";
public static final String SENSOR_FLOW_COUNT = "flowSensorCount";
public static final String SENSOR_RAIN = "rainsensor";
public static final String SENSOR_2 = "sensor2";
public static final String SENSOR_WATERLEVEL = "waterlevel";
public static final String SENSOR_CURRENT_DRAW = "currentDraw";
public static final String CHANNEL_PROGRAMS = "programs";
public static final String CHANNEL_ENABLE_PROGRAMS = "enablePrograms";
public static final String CHANNEL_STATIONS = "stations";
public static final String CHANNEL_RESET_STATIONS = "resetStations";
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";
public static final String CHANNEL_IGNORE_RAIN = "ignoreRain";
public static final String CHANNEL_RAIN_DELAY = "rainDelay";
}

View File

@@ -18,6 +18,8 @@ import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiFactory;
import org.openhab.binding.opensprinkler.internal.handler.OpenSprinklerDeviceHandler;
import org.openhab.binding.opensprinkler.internal.handler.OpenSprinklerHttpBridgeHandler;
@@ -40,14 +42,18 @@ import org.osgi.service.component.annotations.Reference;
* @author Florian Schmidt - Split channels to their own things
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.opensprinkler")
@NonNullByDefault
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 final OpenSprinklerStateDescriptionProvider stateDescriptionProvider;
private OpenSprinklerApiFactory apiFactory;
@Activate
public OpenSprinklerHandlerFactory(@Reference OpenSprinklerApiFactory apiFactory) {
public OpenSprinklerHandlerFactory(@Reference OpenSprinklerApiFactory apiFactory,
final @Reference OpenSprinklerStateDescriptionProvider stateDescriptionProvider) {
this.apiFactory = apiFactory;
this.stateDescriptionProvider = stateDescriptionProvider;
}
@Override
@@ -56,7 +62,7 @@ public class OpenSprinklerHandlerFactory extends BaseThingHandlerFactory {
}
@Override
protected ThingHandler createHandler(Thing thing) {
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(OPENSPRINKLER_HTTP_BRIDGE)) {
@@ -64,7 +70,7 @@ public class OpenSprinklerHandlerFactory extends BaseThingHandlerFactory {
} else if (thingTypeUID.equals(OPENSPRINKLER_STATION)) {
return new OpenSprinklerStationHandler(thing);
} else if (thingTypeUID.equals(OPENSPRINKLER_DEVICE)) {
return new OpenSprinklerDeviceHandler(thing);
return new OpenSprinklerDeviceHandler(thing, stateDescriptionProvider);
}
return null;

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2021 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 java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.types.StateOption;
import com.google.gson.annotations.SerializedName;
/**
* The {@link OpenSprinklerState} class holds the state and replies for an OpenSprinkler device.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
public class OpenSprinklerState {
public JcResponse jcReply = new JcResponse();
public JoResponse joReply = new JoResponse();
public JsResponse jsReply = new JsResponse();
public JpResponse jpReply = new JpResponse();
public JnResponse jnReply = new JnResponse();
public List<StateOption> programs = new ArrayList<>();
public List<StateOption> stations = new ArrayList<>();
public static class JsResponse {
public int sn[] = new int[8];
public int nstations = 8;
}
public static class JpResponse {
public int nprogs = 0;
public Object[] pd = {};
}
public static class JoResponse {
public int wl;
public int fwv = -1;
}
public static class JcResponse {
public @Nullable List<List<Integer>> ps;
@SerializedName(value = "sn1", alternate = "rs")
public int rs;
public long devt = 0;
public long rdst = 0;
public int en = 1;
public int sn2 = -1;
@SerializedName(value = "RSSI", alternate = "rssi") // json reply uses all uppercase
public int rssi = 1;
public int flcrt = -1;
public int curr = -1;
}
public static class JnResponse {
public List<String> snames = new ArrayList<>();
@SerializedName(value = "ignore_rain", alternate = "ignoreRain")
public byte[] ignoreRain = { 0 };
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2021 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.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link OpenSprinklerStateDescriptionProvider} Allows the dynamic updating of Programs that can be run from an
* Opensprinkler Device
*
* @author Matthew Skinner - Initial contribution
*/
@Component(service = { DynamicStateDescriptionProvider.class, OpenSprinklerStateDescriptionProvider.class })
@NonNullByDefault
public class OpenSprinklerStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
@Activate
public OpenSprinklerStateDescriptionProvider(
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
}

View File

@@ -13,19 +13,30 @@
package org.openhab.binding.opensprinkler.internal.api;
import java.math.BigDecimal;
import java.util.List;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.opensprinkler.internal.OpenSprinklerState.JnResponse;
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.api.exception.UnauthorizedApiException;
import org.openhab.binding.opensprinkler.internal.model.StationProgram;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.types.Command;
import org.openhab.core.types.StateOption;
/**
* The {@link OpenSprinklerApi} interface defines the functions which are
* controllable on the OpenSprinkler API interface.
*
* @author Chris Graham - Initial contribution
* @author Florian Schmidt - Refactoring
*/
@NonNullByDefault
public interface OpenSprinklerApi {
/**
* Whether the device entered manual mode and accepts API requests to control the stations.
*
@@ -38,14 +49,14 @@ public interface OpenSprinklerApi {
*
* @throws Exception
*/
public abstract void enterManualMode() throws CommunicationApiException;
public abstract void enterManualMode() throws CommunicationApiException, UnauthorizedApiException;
/**
* Disables the manual mode, if it is enabled.
*
* @throws Exception
*/
public abstract void leaveManualMode() throws CommunicationApiException;
public abstract void leaveManualMode() throws CommunicationApiException, UnauthorizedApiException;
/**
* Starts a station on the OpenSprinkler device for the specified duration.
@@ -72,7 +83,7 @@ public interface OpenSprinklerApi {
* @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;
public abstract boolean isStationOpen(int station) throws CommunicationApiException, GeneralApiException;
/**
* Returns the current program data of the requested station.
@@ -89,34 +100,63 @@ public interface OpenSprinklerApi {
* @return True if rain is detected, false if not or cannot determine.
* @throws Exception
*/
public abstract boolean isRainDetected() throws CommunicationApiException;
public abstract boolean isRainDetected();
/**
* Returns the current draw of all connected zones of the OpenSprinkler device in milliamperes.
*
* @return current draw in milliamperes
* @throws CommunicationApiException
* @throws
* @return current draw in milliamperes or -1 if sensor not supported
*/
public abstract int currentDraw() throws CommunicationApiException, NoCurrentDrawSensorException;
public abstract int currentDraw();
/**
* Returns the state of the second sensor.
*
* @return 1: sensor is active; 0: sensor is inactive; -1: no sensor.
*/
public abstract int getSensor2State();
/**
*
* @return The Wifi signal strength in -dB or 0 if not supported by firmware
*/
public abstract int signalStrength();
/**
*
* @return The pulses that the flow sensor has given in the last time period, -1 if not supported.
*/
public abstract int flowSensorCount();
/**
* CLOSES all stations turning them all off.
*
*/
public abstract void resetStations() throws UnauthorizedApiException, CommunicationApiException;
/**
* Returns true if the internal programs are allowed to auto start.
*
* @return true if enabled
*/
public abstract boolean getIsEnabled();
public abstract void enablePrograms(Command command) throws UnauthorizedApiException, CommunicationApiException;
/**
* Returns the water level in %.
*
* @return waterLevel in %
* @throws CommunicationApiException
* @throws
*/
public abstract int waterLevel() throws CommunicationApiException;
public abstract int waterLevel();
/**
* 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;
public abstract int getNumberOfStations();
/**
* Returns the firmware version number.
@@ -124,5 +164,89 @@ public interface OpenSprinklerApi {
* @return The firmware version of the OpenSprinkler device as an int.
* @throws Exception
*/
public abstract int getFirmwareVersion() throws CommunicationApiException;
public abstract int getFirmwareVersion() throws CommunicationApiException, UnauthorizedApiException;
/**
* Sends all the GET requests and stores/cache the responses for use by the API to prevent the need for multiple
* requests.
*
* @throws CommunicationApiException
* @throws UnauthorizedApiException
*/
public abstract void refresh() throws CommunicationApiException, UnauthorizedApiException;
/**
* Ask the OpenSprinkler for the program names and store these for future use in a List.
*
* @throws CommunicationApiException
* @throws UnauthorizedApiException
*/
public abstract void getProgramData() throws CommunicationApiException, UnauthorizedApiException;
/**
* Returns a list of all internal programs as a list of StateOptions.
*
* @return List<StateOption>
*/
public abstract List<StateOption> getPrograms();
/**
* Return a list of all the stations the device has as List of StateOptions
*
* @return List<StateOption>
*/
public abstract List<StateOption> getStations();
/**
* Runs a Program that is setup and stored inside the OpenSprinkler
*
* @param Program index number that you wish to run.
*
* @throws CommunicationApiException
* @throws UnauthorizedApiException
*/
public abstract void runProgram(Command command) throws CommunicationApiException, UnauthorizedApiException;
/**
* Fetch the station names and place them in a list of List<StateOption>.
* Use getStations() to retrieve this list.
*
* @throws CommunicationApiException
* @throws UnauthorizedApiException
*/
public abstract JnResponse getStationNames() throws CommunicationApiException, UnauthorizedApiException;
/**
* Tells a single station to ignore the rain delay.
*
* @param station
* @param command
* @throws CommunicationApiException
* @throws UnauthorizedApiException
*/
public void ignoreRain(int station, boolean command) throws CommunicationApiException, UnauthorizedApiException;
/**
* Asks if a single station is set to ignore rain delays.
*
* @param station
* @return
*/
public abstract boolean isIgnoringRain(int station);
/**
* Sets how long the OpenSprinkler device will stop running programs for.
*
* @param hours
* @throws UnauthorizedApiException
* @throws CommunicationApiException
*/
public abstract void setRainDelay(int hours) throws UnauthorizedApiException, CommunicationApiException;
/**
* Gets the rain delay in hours from the OpenSprinkler device.
*
* @return QuantityType<Time>
*/
public abstract QuantityType<Time> getRainDelay();
}

View File

@@ -1,50 +0,0 @@
/**
* Copyright (c) 2010-2021 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

@@ -12,7 +12,7 @@
*/
package org.openhab.binding.opensprinkler.internal.api;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.GeneralApiException;
@@ -21,6 +21,8 @@ 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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link OpenSprinklerApiFactory} class is used for creating instances of
@@ -31,9 +33,10 @@ import org.osgi.service.component.annotations.Reference;
* @author Florian Schmidt - Refactoring
*/
@Component(service = OpenSprinklerApiFactory.class)
@NonNullByDefault
public class OpenSprinklerApiFactory {
private @NonNull HttpClient httpClient;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private HttpClient httpClient;
@Activate
public OpenSprinklerApiFactory(@Reference HttpClientFactory httpClientFactory) {
@@ -61,13 +64,17 @@ public class OpenSprinklerApiFactory {
version = lowestSupportedApi.getFirmwareVersion();
} catch (CommunicationApiException exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
"Problem fetching the firmware version from the OpenSprinkler: " + exp.getMessage());
}
logger.debug("Firmware was reported as {}", version);
if (version >= 210 && version < 213) {
return new OpenSprinklerHttpApiV210(this.httpClient, config);
} else if (version >= 213) {
} else if (version >= 213 && version < 217) {
return new OpenSprinklerHttpApiV213(this.httpClient, config);
} else if (version >= 217 && version < 219) {
return new OpenSprinklerHttpApiV217(this.httpClient, config);
} else if (version >= 219) {
return new OpenSprinklerHttpApiV219(this.httpClient, config);
} else {
/* Need to make sure we have an older OpenSprinkler device by checking the first station. */
try {
@@ -77,7 +84,6 @@ public class OpenSprinklerApiFactory {
"There was a problem in the HTTP communication with the OpenSprinkler API: "
+ exp.getMessage());
}
return lowestSupportedApi;
}
}

View File

@@ -12,31 +12,47 @@
*/
package org.openhab.binding.opensprinkler.internal.api;
import static org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiConstants.*;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.*;
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.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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.OpenSprinklerState;
import org.openhab.binding.opensprinkler.internal.OpenSprinklerState.JcResponse;
import org.openhab.binding.opensprinkler.internal.OpenSprinklerState.JnResponse;
import org.openhab.binding.opensprinkler.internal.OpenSprinklerState.JoResponse;
import org.openhab.binding.opensprinkler.internal.OpenSprinklerState.JsResponse;
import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.GeneralApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.UnauthorizedApiException;
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 org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.Command;
import org.openhab.core.types.StateOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.google.gson.JsonSyntaxException;
/**
* The {@link OpenSprinklerHttpApiV100} class is used for communicating with the
@@ -45,19 +61,16 @@ import com.google.gson.annotations.SerializedName;
* @author Chris Graham - Initial contribution
* @author Florian Schmidt - Allow https URLs and basic auth
*/
@NonNullByDefault
class OpenSprinklerHttpApiV100 implements OpenSprinklerApi {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
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 final OpenSprinklerHttpInterfaceConfig config;
protected String password;
protected OpenSprinklerState state = new OpenSprinklerState();
protected int numberOfStations = DEFAULT_STATION_COUNT;
protected boolean isInManualMode = false;
private final Gson gson = new Gson();
protected final Gson gson = new Gson();
protected HttpRequestSender http;
/**
@@ -73,27 +86,15 @@ class OpenSprinklerHttpApiV100 implements OpenSprinklerApi {
* @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.");
}
throws CommunicationApiException, UnauthorizedApiException {
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.config = config;
this.password = config.password;
this.basicUsername = config.basicUsername;
this.basicPassword = config.basicPassword;
this.http = new HttpRequestSender(httpClient);
}
@@ -103,127 +104,139 @@ class OpenSprinklerHttpApiV100 implements OpenSprinklerApi {
}
@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());
}
public List<StateOption> getPrograms() {
return state.programs;
}
this.firmwareVersion = getFirmwareVersion();
this.numberOfStations = getNumberOfStations();
@Override
public List<StateOption> getStations() {
return state.stations;
}
@Override
public void refresh() throws CommunicationApiException, UnauthorizedApiException {
state.joReply = getOptions();
state.jsReply = getStationStatus();
state.jcReply = statusInfo();
state.jnReply = getStationNames();
}
@Override
public void enterManualMode() throws CommunicationApiException, UnauthorizedApiException {
http.sendHttpGet(getBaseUrl(), getRequestRequiredOptions() + "&" + CMD_ENABLE_MANUAL_MODE);
numberOfStations = getNumberOfStations();
isInManualMode = true;
}
@Override
public void leaveManualMode() throws CommunicationApiException {
public void leaveManualMode() throws CommunicationApiException, UnauthorizedApiException {
http.sendHttpGet(getBaseUrl(), getRequestRequiredOptions() + "&" + CMD_DISABLE_MANUAL_MODE);
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());
}
http.sendHttpGet(getBaseUrl() + "sn" + station + "=1&t=" + duration, null);
}
@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");
public boolean isStationOpen(int station) throws CommunicationApiException, GeneralApiException {
String returnContent = http.sendHttpGet(getBaseUrl() + "sn" + station, null);
return "1".equals(returnContent);
}
@Override
public boolean isRainDetected() throws CommunicationApiException {
if (statusInfo().rs == 1) {
return true;
public void ignoreRain(int station, boolean command) throws CommunicationApiException, UnauthorizedApiException {
}
@Override
public boolean isIgnoringRain(int station) {
return false;
}
@Override
public boolean isRainDetected() {
return state.jcReply.rs == 1;
}
@Override
public int getSensor2State() {
return state.jcReply.sn2;
}
@Override
public int currentDraw() {
return state.jcReply.curr;
}
@Override
public int flowSensorCount() {
return state.jcReply.flcrt;
}
@Override
public int signalStrength() {
return state.jcReply.rssi;
}
@Override
public boolean getIsEnabled() {
return state.jcReply.en == 1;
}
@Override
public int waterLevel() {
return state.joReply.wl;
}
@Override
public int getNumberOfStations() {
numberOfStations = state.jsReply.nstations;
return numberOfStations;
}
@Override
public int getFirmwareVersion() throws CommunicationApiException, UnauthorizedApiException {
state.joReply = getOptions();
return state.joReply.fwv;
}
@Override
public void runProgram(Command command) throws CommunicationApiException, UnauthorizedApiException {
logger.warn("OpenSprinkler requires at least firmware 217 for the runProgram feature to work");
}
@Override
public void enablePrograms(Command command) throws UnauthorizedApiException, CommunicationApiException {
if (command == OnOffType.ON) {
http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&en=1");
} else {
return false;
http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&en=0");
}
}
@Override
public int currentDraw() throws CommunicationApiException, NoCurrentDrawSensorException {
JcResponse info = statusInfo();
if (info.curr == null) {
throw new NoCurrentDrawSensorException();
}
return info.curr;
public void resetStations() throws UnauthorizedApiException, CommunicationApiException {
http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&rsn=1");
}
@Override
public int waterLevel() throws CommunicationApiException {
JoResponse info = getOptions();
return info.wl;
public void setRainDelay(int hours) throws UnauthorizedApiException, CommunicationApiException {
http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&rd=" + hours);
}
@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());
public QuantityType<Time> getRainDelay() {
if (state.jcReply.rdst == 0) {
return new QuantityType<Time>(0, Units.SECOND);
}
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;
long remainingTime = state.jcReply.rdst - state.jcReply.devt;
return new QuantityType<Time>(remainingTime, Units.SECOND);
}
/**
@@ -232,7 +245,7 @@ class OpenSprinklerHttpApiV100 implements OpenSprinklerApi {
* @return String representation of the OpenSprinkler API URL.
*/
protected String getBaseUrl() {
return hostname + ":" + port + "/";
return hostname + ":" + config.port + "/";
}
/**
@@ -246,49 +259,89 @@ class OpenSprinklerHttpApiV100 implements OpenSprinklerApi {
@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);
if (state.jcReply.ps != null) {
return state.jcReply.ps.stream().map(values -> new StationProgram(values.get(1)))
.collect(Collectors.toList()).get(station);
}
return new StationProgram(0);
}
private JcResponse statusInfo() throws CommunicationApiException {
private JcResponse statusInfo() throws CommunicationApiException, UnauthorizedApiException {
String returnContent;
JcResponse resp;
try {
returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATUS_INFO, getRequestRequiredOptions());
} catch (CommunicationApiException exp) {
resp = gson.fromJson(returnContent, JcResponse.class);
if (resp == null) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication: jcReply was empty.");
}
} catch (JsonSyntaxException exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
"There was a JSON syntax 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 {
private JoResponse getOptions() throws CommunicationApiException, UnauthorizedApiException {
String returnContent;
JoResponse resp;
try {
returnContent = http.sendHttpGet(getBaseUrl() + CMD_OPTIONS_INFO, getRequestRequiredOptions());
} catch (CommunicationApiException exp) {
resp = gson.fromJson(returnContent, JoResponse.class);
if (resp == null) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication: joReply was empty.");
}
} catch (JsonSyntaxException exp) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
"There was a JSON syntax 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;
protected JsResponse getStationStatus() throws CommunicationApiException, UnauthorizedApiException {
String returnContent;
JsResponse resp;
try {
returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATION_INFO, getRequestRequiredOptions());
resp = gson.fromJson(returnContent, JsResponse.class);
if (resp == null) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication: jsReply was empty.");
}
} catch (JsonSyntaxException exp) {
throw new CommunicationApiException(
"There was a JSON syntax problem in the HTTP communication with the OpenSprinkler API: "
+ exp.getMessage());
}
return resp;
}
@Override
public void getProgramData() throws CommunicationApiException, UnauthorizedApiException {
}
@Override
public JnResponse getStationNames() throws CommunicationApiException, UnauthorizedApiException {
String returnContent;
JnResponse resp;
try {
returnContent = http.sendHttpGet(getBaseUrl() + "jn", getRequestRequiredOptions());
resp = gson.fromJson(returnContent, JnResponse.class);
if (resp == null) {
throw new CommunicationApiException(
"There was a problem in the HTTP communication: jnReply was empty.");
}
} catch (JsonSyntaxException exp) {
throw new CommunicationApiException(
"There was a JSON syntax problem in the HTTP communication with the OpenSprinkler API: "
+ exp.getMessage());
}
state.jnReply = resp;
return resp;
}
/**
@@ -318,35 +371,37 @@ class OpenSprinklerHttpApiV100 implements OpenSprinklerApi {
* @return String contents of the response for the GET request.
* @throws Exception
*/
public String sendHttpGet(String url, String urlParameters) throws CommunicationApiException {
public String sendHttpGet(String url, @Nullable String urlParameters)
throws CommunicationApiException, UnauthorizedApiException {
String location = null;
if (urlParameters != null) {
location = url + "?" + urlParameters;
} else {
location = url;
}
ContentResponse response;
try {
response = withGeneralProperties(httpClient.newRequest(location)).method(HttpMethod.GET).send();
response = withGeneralProperties(httpClient.newRequest(location)).timeout(5, TimeUnit.SECONDS)
.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();
String content = response.getContentAsString();
if ("{\"result\":2}".equals(content)) {
throw new UnauthorizedApiException("Unauthorized, check your password is correct");
}
return content;
}
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));
if (!config.basicUsername.isEmpty() && !config.basicPassword.isEmpty()) {
String encoded = Base64.getEncoder().encodeToString(
(config.basicUsername + ":" + config.basicPassword).getBytes(StandardCharsets.UTF_8));
request.header(HttpHeader.AUTHORIZATION, "Basic " + encoded);
}
return request;
@@ -370,12 +425,10 @@ class OpenSprinklerHttpApiV100 implements OpenSprinklerApi {
} 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

@@ -12,11 +12,15 @@
*/
package org.openhab.binding.opensprinkler.internal.api;
import static org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiConstants.*;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.opensprinkler.internal.OpenSprinklerState.JpResponse;
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;
@@ -29,6 +33,7 @@ import org.openhab.binding.opensprinkler.internal.api.exception.UnauthorizedApiE
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;
import org.openhab.core.types.StateOption;
/**
* The {@link OpenSprinklerHttpApiV210} class is used for communicating with
@@ -37,6 +42,7 @@ import org.openhab.binding.opensprinkler.internal.util.Parse;
* @author Chris Graham - Initial contribution
* @author Florian Schmidt - Refactor class visibility
*/
@NonNullByDefault
class OpenSprinklerHttpApiV210 extends OpenSprinklerHttpApiV100 {
/**
* Constructor for the OpenSprinkler API class to create a connection to the OpenSprinkler
@@ -47,40 +53,50 @@ class OpenSprinklerHttpApiV210 extends OpenSprinklerHttpApiV100 {
* @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 CommunicationApiException
* @throws Exception
*/
OpenSprinklerHttpApiV210(final HttpClient httpClient, final OpenSprinklerHttpInterfaceConfig config)
throws GeneralApiException {
throws GeneralApiException, CommunicationApiException {
super(httpClient, config);
}
@Override
public boolean isStationOpen(int station) throws GeneralApiException, CommunicationApiException {
public void getProgramData() throws CommunicationApiException, UnauthorizedApiException {
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());
returnContent = http.sendHttpGet(getBaseUrl() + CMD_PROGRAM_DATA, 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());
JpResponse resp = gson.fromJson(returnContent, JpResponse.class);
if (resp != null && resp.pd.length > 0) {
state.programs = new ArrayList<>();
int counter = 0;
for (Object x : resp.pd) {
String temp = x.toString();
temp = temp.substring(temp.lastIndexOf(',') + 2, temp.length() - 1);
state.programs.add(new StateOption(Integer.toString(counter++), temp));
}
}
}
if (stationStatus == 1) {
return true;
@Override
public List<StateOption> getStations() {
int counter = 0;
for (String x : state.jnReply.snames) {
state.stations.add(new StateOption(Integer.toString(counter++), x));
}
return state.stations;
}
@Override
public boolean isStationOpen(int station) throws GeneralApiException, CommunicationApiException {
if (state.jsReply.sn.length > 0) {
return state.jsReply.sn[station] == 1;
} else {
return false;
throw new GeneralApiException("There was a problem parsing the station status for the sn value.");
}
}
@@ -88,11 +104,6 @@ class OpenSprinklerHttpApiV210 extends OpenSprinklerHttpApiV100 {
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);
@@ -100,7 +111,6 @@ class OpenSprinklerHttpApiV210 extends OpenSprinklerHttpApiV100 {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
}
resultParser(returnContent);
}
@@ -108,11 +118,6 @@ class OpenSprinklerHttpApiV210 extends OpenSprinklerHttpApiV100 {
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);
@@ -120,7 +125,6 @@ class OpenSprinklerHttpApiV210 extends OpenSprinklerHttpApiV100 {
throw new CommunicationApiException(
"There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
}
resultParser(returnContent);
}
@@ -130,10 +134,8 @@ class OpenSprinklerHttpApiV210 extends OpenSprinklerHttpApiV100 {
* @throws Exception
*/
@Override
public void enterManualMode() throws CommunicationApiException {
this.firmwareVersion = getFirmwareVersion();
this.numberOfStations = getNumberOfStations();
public void enterManualMode() throws CommunicationApiException, UnauthorizedApiException {
numberOfStations = getNumberOfStations();
isInManualMode = true;
}

View File

@@ -12,7 +12,9 @@
*/
package org.openhab.binding.opensprinkler.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.binding.opensprinkler.internal.util.Hash;
@@ -23,6 +25,7 @@ import org.openhab.binding.opensprinkler.internal.util.Hash;
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
class OpenSprinklerHttpApiV213 extends OpenSprinklerHttpApiV210 {
/**
* Constructor for the OpenSprinkler API class to create a connection to the OpenSprinkler
@@ -33,15 +36,14 @@ class OpenSprinklerHttpApiV213 extends OpenSprinklerHttpApiV210 {
* @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 CommunicationApiException
* @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;
throws GeneralApiException, CommunicationApiException {
super(httpClient, config);
password = Hash.getMD5Hash(password);
getProgramData();
getStationNames();
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2021 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;
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.api.exception.UnauthorizedApiException;
import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerHttpInterfaceConfig;
import org.openhab.core.types.Command;
/**
* The {@link OpenSprinklerHttpApiV217} class is used for communicating with
* the OpenSprinkler API for firmware versions 2.1.7 and up.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
public class OpenSprinklerHttpApiV217 extends OpenSprinklerHttpApiV213 {
OpenSprinklerHttpApiV217(final HttpClient httpClient, final OpenSprinklerHttpInterfaceConfig config)
throws GeneralApiException, CommunicationApiException {
super(httpClient, config);
}
@Override
public void runProgram(Command command) throws UnauthorizedApiException, CommunicationApiException {
http.sendHttpGet(getBaseUrl() + "mp", getRequestRequiredOptions() + "&pid=" + command);
}
}

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2021 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;
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.api.exception.UnauthorizedApiException;
import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerHttpInterfaceConfig;
/**
* The {@link OpenSprinklerHttpApiV219} class is used for communicating with
* the firmware versions 2.1.9 and up.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
public class OpenSprinklerHttpApiV219 extends OpenSprinklerHttpApiV217 {
OpenSprinklerHttpApiV219(final HttpClient httpClient, final OpenSprinklerHttpInterfaceConfig config)
throws GeneralApiException, CommunicationApiException {
super(httpClient, config);
}
@Override
public void ignoreRain(int station, boolean command) throws CommunicationApiException, UnauthorizedApiException {
int arrayIndex = station / 8;
int bit = station % 8;
logger.debug("Ignore Rain for Station:{} is being looked in index: {} and bit:{}", station, arrayIndex, bit);
byte status = state.jnReply.ignoreRain[arrayIndex];
if (command) {
status |= 1 << bit;
} else {
status &= ~(1 << bit);
}
http.sendHttpGet(getBaseUrl() + "cs", getRequestRequiredOptions() + "&i" + arrayIndex + "=" + status);
}
@Override
public boolean isIgnoringRain(int station) {
int arrayIndex = station / 8;
int bit = station % 8;
byte status = state.jnReply.ignoreRain[arrayIndex];
return (status & (1 << bit)) != 0;
}
}

View File

@@ -12,12 +12,15 @@
*/
package org.openhab.binding.opensprinkler.internal.api.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link CommunicationApiException} exception is thrown when result from the OpenSprinkler
* API is problems communicating with the controller.
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class CommunicationApiException extends Exception {
/**
* Serial ID of this error class.

View File

@@ -12,12 +12,15 @@
*/
package org.openhab.binding.opensprinkler.internal.api.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link DataFormatErrorApiException} exception is thrown when result from the OpenSprinkler
* API is "result" : 18.
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class DataFormatErrorApiException extends GeneralApiException {
/**
* Serial ID of this error class.

View File

@@ -12,12 +12,15 @@
*/
package org.openhab.binding.opensprinkler.internal.api.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link DataMissingApiException} exception is thrown when result from the OpenSprinkler
* API is "result" : 16.
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class DataMissingApiException extends GeneralApiException {
/**
* Serial ID of this error class.

View File

@@ -12,12 +12,15 @@
*/
package org.openhab.binding.opensprinkler.internal.api.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link GeneralApiException} exception is thrown when problems
* working with the OpenSprinkler API arise.
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class GeneralApiException extends Exception {
/**
* Serial ID of this error class.

View File

@@ -12,12 +12,15 @@
*/
package org.openhab.binding.opensprinkler.internal.api.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link MismatchApiException} exception is thrown when result from the OpenSprinkler
* API is "result" : 3.
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class MismatchApiException extends GeneralApiException {
/**
* Serial ID of this error class.

View File

@@ -12,12 +12,15 @@
*/
package org.openhab.binding.opensprinkler.internal.api.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link NotPermittedApiException} exception is thrown when result from the OpenSprinkler
* API is "result" : 48.
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class NotPermittedApiException extends GeneralApiException {
/**
* Serial ID of this error class.

View File

@@ -12,12 +12,15 @@
*/
package org.openhab.binding.opensprinkler.internal.api.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link OutOfRangeApiException} exception is thrown when result from the OpenSprinkler
* API is "result" : 17.
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class OutOfRangeApiException extends GeneralApiException {
/**
* Serial ID of this error class.

View File

@@ -12,12 +12,15 @@
*/
package org.openhab.binding.opensprinkler.internal.api.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link PageNotFoundApiException} exception is thrown when result from the OpenSprinkler
* API is "result" : 32.
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class PageNotFoundApiException extends GeneralApiException {
/**
* Serial ID of this error class.

View File

@@ -12,12 +12,15 @@
*/
package org.openhab.binding.opensprinkler.internal.api.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link UnauthorizedApiException} exception is thrown when result from the OpenSprinkler
* API is "result" : 2.
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class UnauthorizedApiException extends GeneralApiException {
/**
* Serial ID of this error class.

View File

@@ -12,12 +12,15 @@
*/
package org.openhab.binding.opensprinkler.internal.api.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link UnknownApiException} exception is thrown when result from the OpenSprinkler
* API returns an unknown result.
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class UnknownApiException extends GeneralApiException {
/**
* Serial ID of this error class.

View File

@@ -12,8 +12,9 @@
*/
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.*;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.DEFAULT_ADMIN_PASSWORD;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link OpenSprinklerHttpInterfaceConfig} class defines the configuration options
@@ -21,16 +22,17 @@ import static org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiCon
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class OpenSprinklerHttpInterfaceConfig {
/**
* Hostname of the OpenSprinkler API.
*/
public String hostname = null;
public String hostname = "";
/**
* The port the OpenSprinkler API is listening on.
*/
public int port = DEFAULT_API_PORT;
public int port = 80;
/**
* The password to connect to the OpenSprinkler API.
@@ -40,13 +42,13 @@ public class OpenSprinklerHttpInterfaceConfig {
/**
* Number of seconds in between refreshes from the OpenSprinkler device.
*/
public int refresh = DEFAULT_REFRESH_RATE;
public int refresh = 60;
/**
* The basic auth username to use when the OpenSprinkler device is behind a reverse proxy with basic auth enabled.
*/
public String basicUsername = null;
public String basicUsername = "";
/**
* The basic auth password to use when the OpenSprinkler device is behind a reverse proxy with basic auth enabled.
*/
public String basicPassword = null;
public String basicPassword = "";
}

View File

@@ -12,8 +12,9 @@
*/
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;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link OpenSprinklerPiConfig} class defines the configuration options
@@ -21,6 +22,7 @@ import static org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiCon
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class OpenSprinklerPiConfig {
/**
* Number of stations to control.

View File

@@ -12,15 +12,18 @@
*/
package org.openhab.binding.opensprinkler.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link OpenSprinklerStationConfig} class defines the configuration options
* for the OpenSprinkler Thing.
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class OpenSprinklerStationConfig {
/**
* The index of the station the thing is configured to control, starting with 0.
*/
public int stationIndex = -1;
public int stationIndex = 0;
}

View File

@@ -12,15 +12,10 @@
*/
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.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.GeneralApiException;
import org.openhab.binding.opensprinkler.internal.api.exception.UnauthorizedApiException;
import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerHttpInterfaceConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,11 +27,10 @@ import org.slf4j.LoggerFactory;
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
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) {
@@ -59,46 +53,15 @@ public class OpenSprinklerDiscoveryJob implements Runnable {
*/
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) {
OpenSprinklerHttpInterfaceConfig config = new OpenSprinklerHttpInterfaceConfig();
config.hostname = ip;
discoveryClass.getApiFactory().getHttpApi(config);
} catch (UnauthorizedApiException e) {
return true;
} catch (CommunicationApiException | GeneralApiException 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;
return true;
}
}

View File

@@ -13,17 +13,15 @@
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.io.IOException;
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.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -31,7 +29,7 @@ import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.commons.net.util.SubnetUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiFactory;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
@@ -51,12 +49,12 @@ import org.slf4j.LoggerFactory;
* @author Chris Graham - Initial contribution
*/
@Component(service = DiscoveryService.class, configurationPid = "discovery.opensprinkler")
@NonNullByDefault
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 ExecutorService discoverySearchPool = scheduler;
private OpenSprinklerApiFactory apiFactory;
@Activate
@@ -76,23 +74,13 @@ public class OpenSprinklerDiscoveryService extends AbstractDiscoveryService {
@Override
protected void startScan() {
logger.debug("Starting discovery of OpenSprinkler devices.");
discoverySearchPool = Executors.newFixedThreadPool(DISCOVERY_THREAD_POOL_SIZE);
try {
List<String> ipList = getIpAddressScanList();
discoverySearchPool = Executors.newFixedThreadPool(DISCOVERY_THREAD_POOL_SIZE);
for (String ip : ipList) {
discoverySearchPool.execute(new OpenSprinklerDiscoveryJob(this, ip));
}
discoverySearchPool.shutdown();
ipAddressScan();
} catch (Exception exp) {
logger.debug("OpenSprinkler discovery service encountered an error while scanning for devices: {}",
exp.getMessage());
}
logger.debug("Completed discovery of OpenSprinkler devices.");
}
@@ -102,52 +90,68 @@ public class OpenSprinklerDiscoveryService extends AbstractDiscoveryService {
* @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('.', '_'));
ThingUID bridgeUID = 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("port", 80);
properties.put("password", DEFAULT_ADMIN_PASSWORD);
properties.put("refresh", DEFAULT_REFRESH_RATE);
thingDiscovered(
DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel("OpenSprinkler").build());
properties.put("refresh", 60);
thingDiscovered(DiscoveryResultBuilder.create(bridgeUID).withProperties(properties)
.withLabel("OpenSprinkler HTTP Bridge").withRepresentationProperty("hostname").build());
// Now create the Device thing
properties.clear();
properties.put("hostname", ip);
ThingUID uid = new ThingUID(OPENSPRINKLER_DEVICE, bridgeUID, ip.replace('.', '_'));
thingDiscovered(DiscoveryResultBuilder.create(uid).withBridge(bridgeUID).withProperties(properties)
.withRepresentationProperty("hostname").withLabel("OpenSprinkler Device").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);
private void scanSingleSubnet(InterfaceAddress hostAddress) {
byte[] broadcastAddress = hostAddress.getBroadcast().getAddress();
// Create subnet mask from length
int shft = 0xffffffff << (32 - hostAddress.getNetworkPrefixLength());
byte oct1 = (byte) (((byte) ((shft & 0xff000000) >> 24)) & 0xff);
byte oct2 = (byte) (((byte) ((shft & 0x00ff0000) >> 16)) & 0xff);
byte oct3 = (byte) (((byte) ((shft & 0x0000ff00) >> 8)) & 0xff);
byte oct4 = (byte) (((byte) (shft & 0x000000ff)) & 0xff);
byte[] subnetMask = new byte[] { oct1, oct2, oct3, oct4 };
// calc first IP to start scanning from on this subnet
byte[] startAddress = new byte[4];
startAddress[0] = (byte) (broadcastAddress[0] & subnetMask[0]);
startAddress[1] = (byte) (broadcastAddress[1] & subnetMask[1]);
startAddress[2] = (byte) (broadcastAddress[2] & subnetMask[2]);
startAddress[3] = (byte) (broadcastAddress[3] & subnetMask[3]);
// Loop from start of subnet to the broadcast address.
for (int i = ByteBuffer.wrap(startAddress).getInt(); i < ByteBuffer.wrap(broadcastAddress).getInt(); i++) {
try {
InetAddress currentIP = InetAddress.getByAddress(ByteBuffer.allocate(4).putInt(i).array());
// Try to reach each IP with a timeout of 500ms which is enough for local network
if (currentIP.isReachable(500)) {
String host = currentIP.getHostAddress().toString();
logger.debug("Unknown device was found at: {}", host);
discoverySearchPool.execute(new OpenSprinklerDiscoveryJob(this, host));
}
} catch (IOException e) {
}
}
}
return results;
private void ipAddressScan() {
try {
for (Enumeration<NetworkInterface> enumNetworks = NetworkInterface.getNetworkInterfaces(); enumNetworks
.hasMoreElements();) {
NetworkInterface networkInterface = enumNetworks.nextElement();
List<InterfaceAddress> list = networkInterface.getInterfaceAddresses();
for (InterfaceAddress hostAddress : list) {
InetAddress inetAddress = hostAddress.getAddress();
if (!inetAddress.isLoopbackAddress() && inetAddress.isSiteLocalAddress()) {
logger.debug("Scanning all IP address's that IP {}/{} is on", hostAddress.getAddress(),
hostAddress.getNetworkPrefixLength());
scanSingleSubnet(hostAddress);
}
}
}
} catch (SocketException ex) {
}
}
}

View File

@@ -1,105 +0,0 @@
/**
* Copyright (c) 2010-2021 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

@@ -12,50 +12,50 @@
*/
package org.openhab.binding.opensprinkler.internal.handler;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.MAX_TIME_SECONDS;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApi;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
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;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Chris Graham - Initial contribution
* @author Florian Schmidt - Refactoring
*/
@NonNullByDefault
public abstract class OpenSprinklerBaseHandler extends BaseThingHandler {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
protected BigDecimal nextDurationTime = MAX_TIME_SECONDS;
@Nullable
OpenSprinklerHttpBridgeHandler bridgeHandler;
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)) {
protected @Nullable OpenSprinklerApi getApi() {
OpenSprinklerHttpBridgeHandler localBridge = bridgeHandler;
if (localBridge == null) {
return null;
}
try {
return ((OpenSprinklerBaseBridgeHandler) handler).getApi();
return localBridge.getApi();
} catch (IllegalStateException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, e.getMessage());
return null;
}
}
@@ -69,5 +69,33 @@ public abstract class OpenSprinklerBaseHandler extends BaseThingHandler {
}
}
protected void handleNextDurationCommand(ChannelUID channelUID, Command command) {
if (!(command instanceof QuantityType<?>)) {
logger.warn("Ignoring implausible non-QuantityType command for NEXT_DURATION");
return;
}
QuantityType<?> quantity = (QuantityType<?>) command;
quantity = quantity.toUnit(Units.SECOND);
if (quantity != null) {
nextDurationTime = quantity.toBigDecimal();
updateState(channelUID, quantity);
}
}
protected BigDecimal nextDurationValue() {
return nextDurationTime;
}
@Override
public void initialize() {
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "No HTTP Bridge thing selected");
return;
}
bridgeHandler = (OpenSprinklerHttpBridgeHandler) bridge.getHandler();
updateStatus(ThingStatus.ONLINE);
}
protected abstract void updateChannel(ChannelUID uid);
}

View File

@@ -16,90 +16,228 @@ import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingCon
import static org.openhab.core.library.unit.MetricPrefix.MILLI;
import static org.openhab.core.library.unit.Units.PERCENT;
import java.math.BigDecimal;
import java.util.ArrayList;
import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.ElectricCurrent;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.opensprinkler.internal.OpenSprinklerStateDescriptionProvider;
import org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApi;
import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
import org.openhab.binding.opensprinkler.internal.model.NoCurrentDrawSensorException;
import org.openhab.binding.opensprinkler.internal.api.exception.UnauthorizedApiException;
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.library.types.StringType;
import org.openhab.core.library.unit.Units;
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.binding.builder.ChannelBuilder;
import org.openhab.core.thing.ThingStatusDetail;
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;
import org.openhab.core.types.RefreshType;
/**
* @author Chris Graham - Initial contribution
* @author Florian Schmidt - Refactoring
*/
@NonNullByDefault
public class OpenSprinklerDeviceHandler extends OpenSprinklerBaseHandler {
private final Logger logger = LoggerFactory.getLogger(OpenSprinklerDeviceHandler.class);
public final OpenSprinklerStateDescriptionProvider stateDescriptionProvider;
public OpenSprinklerDeviceHandler(Thing thing) {
public OpenSprinklerDeviceHandler(Thing thing, OpenSprinklerStateDescriptionProvider stateDescriptionProvider) {
super(thing);
this.stateDescriptionProvider = stateDescriptionProvider;
}
@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(Units.AMPERE)));
break;
default:
logger.debug("Not updating unknown channel {}", channel);
}
} catch (CommunicationApiException | NoCurrentDrawSensorException e) {
logger.debug("Could not update {}", channel, e);
OpenSprinklerApi localAPI = getApi();
if (localAPI == null) {
return;
}
switch (channel.getIdWithoutGroup()) {
case SENSOR_RAIN:
if (localAPI.isRainDetected()) {
updateState(channel, OnOffType.ON);
} else {
updateState(channel, OnOffType.OFF);
}
break;
case CHANNEL_RAIN_DELAY:
updateState(channel, localAPI.getRainDelay());
break;
case SENSOR_2:
if (localAPI.getSensor2State() == 1) {
updateState(channel, OnOffType.ON);
} else {
updateState(channel, OnOffType.OFF);
}
break;
case SENSOR_WATERLEVEL:
updateState(channel, QuantityType.valueOf(localAPI.waterLevel(), PERCENT));
break;
case SENSOR_CURRENT_DRAW:
updateState(channel, new QuantityType<ElectricCurrent>(localAPI.currentDraw(), MILLI(Units.AMPERE)));
break;
case SENSOR_SIGNAL_STRENGTH:
int rssiValue = localAPI.signalStrength();
if (rssiValue < -80) {
updateState(channel, DecimalType.ZERO);
} else if (rssiValue < -70) {
updateState(channel, new DecimalType(1));
} else if (rssiValue < -60) {
updateState(channel, new DecimalType(2));
} else if (rssiValue < -40) {
updateState(channel, new DecimalType(3));
} else if (rssiValue >= -40) {
updateState(channel, new DecimalType(4));
}
break;
case SENSOR_FLOW_COUNT:
updateState(channel, new QuantityType<Dimensionless>(localAPI.flowSensorCount(), Units.ONE));
break;
case CHANNEL_PROGRAMS:
break;
case CHANNEL_ENABLE_PROGRAMS:
if (localAPI.getIsEnabled()) {
updateState(channel, OnOffType.ON);
} else {
updateState(channel, OnOffType.OFF);
}
break;
case CHANNEL_STATIONS:
break;
case NEXT_DURATION:
break;
case CHANNEL_RESET_STATIONS:
break;
default:
logger.debug("Can not update the unknown channel {}", channel);
}
}
@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();
OpenSprinklerApi localAPI = getApi();
// Remove channels due to missing sensors or old firmware
if (localAPI != null) {
ArrayList<Channel> removeChannels = new ArrayList<>();
Channel channel = thing.getChannel(SENSOR_CURRENT_DRAW);
if (localAPI.currentDraw() == -1 && channel != null) {
logger.debug("No current sensor detected, removing channel.");
removeChannels.add(channel);
}
channel = thing.getChannel(SENSOR_SIGNAL_STRENGTH);
if (localAPI.signalStrength() == 1 && channel != null) {
removeChannels.add(channel);
}
channel = thing.getChannel(SENSOR_FLOW_COUNT);
if (localAPI.flowSensorCount() == -1 && channel != null) {
removeChannels.add(channel);
}
channel = thing.getChannel(SENSOR_2);
if (localAPI.getSensor2State() == -1 && channel != null) {
removeChannels.add(channel);
}
if (!removeChannels.isEmpty()) {
ThingBuilder thingBuilder = editThing();
thingBuilder.withoutChannels(removeChannels);
updateThing(thingBuilder.build());
}
updateProgramsChanOptions(localAPI);
updateStationsChanOptions(localAPI);
nextDurationTime = new BigDecimal(1800);
updateState(NEXT_DURATION, new QuantityType<>(nextDurationTime, Units.SECOND));
}
}
/**
* Fetch the stored Program list and update the StateOptions on the channel so they match.
*
* @param api
*/
private void updateProgramsChanOptions(OpenSprinklerApi api) {
stateDescriptionProvider.setStateOptions(new ChannelUID(this.getThing().getUID(), CHANNEL_PROGRAMS),
api.getPrograms());
}
private void updateStationsChanOptions(OpenSprinklerApi api) {
stateDescriptionProvider.setStateOptions(new ChannelUID(this.getThing().getUID(), CHANNEL_STATIONS),
api.getStations());
}
protected void handleRainDelayCommand(ChannelUID channelUID, Command command, OpenSprinklerApi api)
throws UnauthorizedApiException, CommunicationApiException {
if (!(command instanceof QuantityType<?>)) {
logger.warn("Ignoring implausible non-QuantityType command for rainDelay.");
return;
}
QuantityType<?> quantity = (QuantityType<?>) command;
quantity = quantity.toUnit(Units.HOUR);
if (quantity != null) {
api.setRainDelay(quantity.intValue());
}
updateStatus(ThingStatus.ONLINE);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// nothing to do here
OpenSprinklerApi api = getApi();
if (api == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "OpenSprinkler bridge returned no API.");
return;
}
OpenSprinklerHttpBridgeHandler localBridge = bridgeHandler;
if (localBridge == null) {
return;
}
try {
if (command instanceof RefreshType) {
switch (channelUID.getIdWithoutGroup()) {
case CHANNEL_PROGRAMS:
api.getProgramData();
updateProgramsChanOptions(api);
break;
case CHANNEL_STATIONS:
api.getStationNames();
updateStationsChanOptions(api);
break;
}
} else {
switch (channelUID.getIdWithoutGroup()) {
case CHANNEL_PROGRAMS:
api.runProgram(command);
break;
case CHANNEL_ENABLE_PROGRAMS:
api.enablePrograms(command);
break;
case NEXT_DURATION:
handleNextDurationCommand(channelUID, command);
break;
case CHANNEL_RESET_STATIONS:
if (command == OnOffType.ON) {
api.resetStations();
}
break;
case CHANNEL_STATIONS:
if (command instanceof StringType) {
BigDecimal temp = new BigDecimal(command.toString());
api.openStation(temp.intValue(), nextDurationValue());
}
break;
case CHANNEL_RAIN_DELAY:
handleRainDelayCommand(channelUID, command, api);
break;
}
localBridge.delayedRefresh();// update sensors and controls after command is sent
}
} catch (Exception e) {
localBridge.communicationError(e);
}
}
}

View File

@@ -12,7 +12,8 @@
*/
package org.openhab.binding.opensprinkler.internal.handler;
import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.DEFAULT_REFRESH_RATE;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -20,22 +21,28 @@ 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.api.exception.UnauthorizedApiException;
import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerHttpInterfaceConfig;
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 Chris Graham - Initial contribution
* @author Florian Schmidt - Refactoring
*/
@NonNullByDefault
public class OpenSprinklerHttpBridgeHandler extends OpenSprinklerBaseBridgeHandler {
public class OpenSprinklerHttpBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(OpenSprinklerHttpBridgeHandler.class);
@Nullable
private OpenSprinklerHttpInterfaceConfig openSprinklerConfig;
private @Nullable ScheduledFuture<?> pollingJob;
private @Nullable ScheduledFuture<?> delayedJob;
private @Nullable OpenSprinklerApi openSprinklerDevice;
private OpenSprinklerHttpInterfaceConfig openSprinklerConfig = new OpenSprinklerHttpInterfaceConfig();
private OpenSprinklerApiFactory apiFactory;
public OpenSprinklerHttpBridgeHandler(Bridge bridge, OpenSprinklerApiFactory apiFactory) {
@@ -43,52 +50,105 @@ public class OpenSprinklerHttpBridgeHandler extends OpenSprinklerBaseBridgeHandl
this.apiFactory = apiFactory;
}
@Override
public void initialize() {
OpenSprinklerHttpInterfaceConfig openSprinklerConfig = getConfig().as(OpenSprinklerHttpInterfaceConfig.class);
this.openSprinklerConfig = openSprinklerConfig;
public OpenSprinklerApi getApi() {
OpenSprinklerApi api = openSprinklerDevice;
if (api == null) {
throw new IllegalStateException();
}
return api;
}
public void communicationError(Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Communication Error with the OpenSprinkler: " + e.getMessage());
}
public void refreshStations() {
OpenSprinklerApi localApi = openSprinklerDevice;
if (localApi == null || !localApi.isManualModeEnabled()) {
setupAPI();
localApi = openSprinklerDevice;
}
if (localApi != null) {
try {
localApi.refresh();
updateStatus(ThingStatus.ONLINE);
this.getThing().getThings().forEach(thing -> {
OpenSprinklerBaseHandler handler = (OpenSprinklerBaseHandler) thing.getHandler();
if (handler != null) {
handler.updateChannels();
}
});
} catch (CommunicationApiException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Could not sync status with the OpenSprinkler. " + e.getMessage());
} catch (UnauthorizedApiException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Unauthorized, check your password is correct");
}
}
}
public void delayedRefresh() {
ScheduledFuture<?> localFuture = delayedJob;
if (localFuture == null || localFuture.isDone()) {
delayedJob = scheduler.schedule(this::refreshStations, 3, TimeUnit.SECONDS);
} else {// User has sent multiple commands quickly, only need to update the controls once.
localFuture.cancel(true);
delayedJob = scheduler.schedule(this::refreshStations, 3, TimeUnit.SECONDS);
}
}
private void setupAPI() {
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;
OpenSprinklerApi localApi = openSprinklerDevice;
localApi.enterManualMode();
if (!localApi.isManualModeEnabled()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Could not initialize the connection to the OpenSprinkler.");
}
} catch (CommunicationApiException | GeneralApiException exp) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Could not create API connection to the OpenSprinkler device. Error received: " + exp);
"Could not create an API connection to the OpenSprinkler. 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;
public void handleCommand(ChannelUID channelUID, Command command) {
// Nothing to do for the bridge handler
}
@Override
public void initialize() {
openSprinklerConfig = getConfig().as(OpenSprinklerHttpInterfaceConfig.class);
pollingJob = scheduler.scheduleWithFixedDelay(this::refreshStations, 2, openSprinklerConfig.refresh,
TimeUnit.SECONDS);
}
@Override
public void dispose() {
OpenSprinklerApi localApi = openSprinklerDevice;
if (localApi != null) {
try {
localApi.leaveManualMode();
} catch (CommunicationApiException | UnauthorizedApiException e) {
logger.warn("Could not close connection on teardown.");
}
openSprinklerDevice = null;
}
ScheduledFuture<?> localFuture = pollingJob;
if (localFuture != null) {
localFuture.cancel(true);
pollingJob = null;
}
localFuture = delayedJob;
if (localFuture != null) {
localFuture.cancel(true);
pollingJob = null;
}
return openSprinklerConfig.refresh;
}
}

View File

@@ -18,7 +18,6 @@ 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;
@@ -28,27 +27,20 @@ import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerStationCon
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
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;
/**
* @author Chris Graham - Initial contribution
* @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;
private OpenSprinklerStationConfig config = new OpenSprinklerStationConfig();
public OpenSprinklerStationHandler(Thing thing) {
super(thing);
@@ -56,7 +48,13 @@ public class OpenSprinklerStationHandler extends OpenSprinklerBaseHandler {
@Override
public void initialize() {
super.initialize();
config = getConfig().as(OpenSprinklerStationConfig.class);
OpenSprinklerApi api = getApi();
if (api != null && config.stationIndex >= api.getNumberOfStations()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Station Index is higher than the number of stations that the OpenSprinkler is reporting. Make sure your Station Index is correct.");
}
}
@Override
@@ -66,68 +64,46 @@ public class OpenSprinklerStationHandler extends OpenSprinklerBaseHandler {
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();
}
@SuppressWarnings("null")
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.toUnit(Units.SECOND).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());
if (command != RefreshType.REFRESH) {
switch (channelUID.getIdWithoutGroup()) {
case NEXT_DURATION:
handleNextDurationCommand(channelUID, command);
break;
case STATION_STATE:
if (!(command instanceof OnOffType)) {
logger.warn("Received invalid command type for OpenSprinkler station ({}).", command);
return;
}
if (command == OnOffType.ON) {
api.openStation(config.stationIndex, nextDurationValue());
} else {
api.closeStation(config.stationIndex);
}
break;
case STATION_QUEUED:
if (command == OnOffType.OFF) {
api.closeStation(config.stationIndex);
}
break;
case CHANNEL_IGNORE_RAIN:
api.ignoreRain(config.stationIndex, command == OnOffType.ON);
break;
}
OpenSprinklerHttpBridgeHandler localBridge = bridgeHandler;
if (localBridge == null) {
return;
}
// update all controls after a command is sent in case a long poll time is set.
localBridge.delayedRefresh();
}
} catch (CommunicationApiException | GeneralApiException exp) {
} catch (GeneralApiException | CommunicationApiException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Could not control the station channel " + (this.getStationIndex() + 1)
+ " for the OpenSprinkler. Error: " + exp.getMessage());
"Could not control the station channel " + (config.stationIndex + 1)
+ " for the OpenSprinkler. Error: " + e.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.
*
@@ -186,9 +162,10 @@ public class OpenSprinklerStationHandler extends OpenSprinklerBaseHandler {
}
@Override
protected void updateChannel(@NonNull ChannelUID channel) {
OnOffType currentDeviceState = getStationState(this.getStationIndex());
protected void updateChannel(ChannelUID channel) {
OnOffType currentDeviceState = getStationState(config.stationIndex);
QuantityType<Time> remainingWaterTime = getRemainingWaterTime(config.stationIndex);
OpenSprinklerApi api = getApi();
switch (channel.getIdWithoutGroup()) {
case STATION_STATE:
if (currentDeviceState != null) {
@@ -202,9 +179,7 @@ public class OpenSprinklerStationHandler extends OpenSprinklerBaseHandler {
break;
case NEXT_DURATION:
BigDecimal duration = nextDurationValue();
if (duration != null) {
updateState(channel, new QuantityType<>(duration, Units.SECOND));
}
updateState(channel, new QuantityType<>(duration, Units.SECOND));
break;
case STATION_QUEUED:
if (remainingWaterTime != null && currentDeviceState != null && currentDeviceState == OnOffType.OFF
@@ -214,20 +189,15 @@ public class OpenSprinklerStationHandler extends OpenSprinklerBaseHandler {
updateState(channel, OnOffType.OFF);
}
break;
case CHANNEL_IGNORE_RAIN:
if (api != null && api.isIgnoringRain(config.stationIndex)) {
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

@@ -1,22 +0,0 @@
/**
* Copyright (c) 2010-2021 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

@@ -12,11 +12,14 @@
*/
package org.openhab.binding.opensprinkler.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link StationProgram} class corresponds to the program set in the station.
*
* @author Florian Schmidt - Initial contribution
*/
@NonNullByDefault
public class StationProgram {
public final long remainingWaterTime;

View File

@@ -14,12 +14,15 @@ package org.openhab.binding.opensprinkler.internal.util;
import java.security.MessageDigest;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link Hash} class contains static methods for creating hashes
* of strings. Usually for password hashing.
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class Hash {
private static final String MD5_HASH_ALGORITHM = "MD5";
private static final String UTF8_CHAR_SET = "UTF-8";
@@ -48,7 +51,8 @@ public class Hash {
return digest;
} catch (Exception exp) {
return null;
// Instead of null we return the unhashed password.
return unhashed;
}
}
}

View File

@@ -15,6 +15,8 @@ package org.openhab.binding.opensprinkler.internal.util;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
@@ -26,6 +28,7 @@ import com.google.gson.JsonParser;
*
* @author Chris Graham - Initial contribution
*/
@NonNullByDefault
public class Parse {
/**
* Parses an integer from a JSON string given its key name.
@@ -37,7 +40,11 @@ public class Parse {
public static int jsonInt(String jsonData, String keyName) {
JsonElement jelement = JsonParser.parseString(jsonData);
JsonObject jobject = jelement.getAsJsonObject();
return jobject.get(keyName).getAsInt();
jelement = jobject.get(keyName);
if (jelement == null) {
return 0;// prevents a NPE if the key does not exist.
}
return jelement.getAsInt();
}
/**

View File

@@ -5,22 +5,24 @@
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>
<label>OpenSprinkler HTTP Bridge</label>
<description>A connection to a stand alone OpenSprinkler device which communicates over HTTP.</description>
<config-description>
<parameter name="hostname" type="text">
<parameter name="hostname" type="text" required="true">
<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">
<parameter name="port" type="integer" min="1" max="65535" required="true">
<label>Port</label>
<description>Port of the OpenSprinkler Web API interface.</description>
<default>80</default>
<advanced>true</advanced>
</parameter>
<parameter name="password" type="text">
<parameter name="password" type="text" required="true">
<context>password</context>
<label>Password</label>
<description>The admin password used to access the Web API interface.</description>
<default>opendoor</default>
@@ -55,12 +57,14 @@
<channel id="queued" typeId="queued"></channel>
<channel id="remainingWaterTime" typeId="remainingWaterTime"></channel>
<channel id="nextDuration" typeId="nextDuration"></channel>
<channel id="ignoreRain" typeId="ignoreRain"></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>
<default>0</default>
</parameter>
</config-description>
</thing-type>
@@ -74,33 +78,87 @@
<channels>
<channel id="rainsensor" typeId="rainsensor"></channel>
<channel id="sensor2" typeId="sensor2"></channel>
<channel id="waterlevel" typeId="waterlevel"></channel>
<channel id="currentDraw" typeId="currentDraw"></channel>
<channel id="signalStrength" typeId="system.signal-strength"></channel>
<channel id="flowSensorCount" typeId="flowSensorCount"></channel>
<channel id="programs" typeId="programs"></channel>
<channel id="stations" typeId="stations"></channel>
<channel id="nextDuration" typeId="nextDuration"></channel>
<channel id="resetStations" typeId="resetStations"></channel>
<channel id="enablePrograms" typeId="enablePrograms"></channel>
<channel id="rainDelay" typeId="rainDelay"></channel>
</channels>
</thing-type>
<channel-type id="rainsensor">
<item-type>Switch</item-type>
<label>Rain</label>
<label>Rain Sensor</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="sensor2">
<item-type>Switch</item-type>
<label>Sensor 2</label>
<description>Sensor 2 can be setup as a rain, flow or soil moisture sensor.</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>
<description>The current watering level in percent</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="flowSensorCount">
<item-type>Number:Dimensionless</item-type>
<label>Flow Sensor Count</label>
<description>A count of how many pulses the water flow sensor has given.</description>
<category>Flow</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="currentDraw">
<item-type>Number:ElectricCurrent</item-type>
<label>Current Draw</label>
<description>The current draw in mA</description>
<category>Energy</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="stationState">
<item-type>Switch</item-type>
<label>Station</label>
<label>Station State</label>
<description>Controls a station on the OpenSprinkler device.</description>
<category>Switch</category>
</channel-type>
<channel-type id="ignoreRain">
<item-type>Switch</item-type>
<label>Station Ignores Rain</label>
<description>The station will ignore forecasted rain.</description>
<category>Switch</category>
</channel-type>
<channel-type id="resetStations">
<item-type>Switch</item-type>
<label>Reset Stations</label>
<description>Resets all stations back to CLOSED.</description>
<category>Switch</category>
</channel-type>
<channel-type id="enablePrograms">
<item-type>Switch</item-type>
<label>Enable Programs</label>
<description>Allow programs to auto run, when OFF, manually started stations still work.</description>
<category>Switch</category>
</channel-type>
<channel-type id="queued">
<item-type>Switch</item-type>
<label>Queued</label>
@@ -113,13 +171,39 @@
<item-type>Number:Time</item-type>
<label>Remaining Water Time</label>
<description>Read-only property of the remaining water time of the station.</description>
<category>Time</category>
<state readOnly="true" pattern="%.0f min"/>
</channel-type>
<channel-type id="nextDuration">
<item-type>Number:Time</item-type>
<label>Next Open Duration</label>
<label>Next Duration</label>
<description>The duration the station will be opened the next time it is switched on.</description>
<category>Time</category>
<state readOnly="false" pattern="%.0f min"/>
</channel-type>
<channel-type id="rainDelay">
<item-type>Number:Time</item-type>
<label>Rain Delay</label>
<description>The amount of time in hours to delay the running of any program.</description>
<category>Time</category>
<state readOnly="false" pattern="%.0f h"/>
</channel-type>
<channel-type id="programs">
<item-type>String</item-type>
<label>Run Program</label>
<description>Run a program that is saved inside the OpenSprinkler Device.</description>
<state readOnly="false">
</state>
</channel-type>
<channel-type id="stations">
<item-type>String</item-type>
<label>Open Station</label>
<description>Opens the solenoid of a single station.</description>
<state readOnly="false">
</state>
</channel-type>
</thing:thing-descriptions>