## Supported Things
-### Cloud Thing
+
+### Account Bridge Thing
-The Cloud Thing type is the primary way most users will control and monitor their irrigation system.
+The Account Bridge Thing type represents the user's account on the Hydrawise cloud service. The bridge can have one or more child [Controllers](#Controller-Thing) linked.
+
+An account must be manually added and configured.
+
+### Controller Thing
+
+Controller Things are automatically discovered once an [Account Bridge](#Account-Bridge-Thing) has be properly configured.
+
+The Controller Thing type is the primary way most users will control and monitor their irrigation system.
This allows full control over zones, sensors and weather forecasts.
Changes made through this Thing type will be reflected in the Hydrawise mobile and web applications as well as in their reporting modules.
-#### Cloud Thing Supported Channel Groups
+Controller Things require a parent [Account Bridge](#Account-Bridge-Thing)
-| channel group ID |
-|---------------------------------------|
-| [Zones](#Zone-Channel-Group) |
-| [All Zones](#All-Zones-Channel-Group) |
-| [Sensor](#Sensor-Channel-Group) |
-| [Forecast](#Sensor-Channel-Group) |
+#### Controller Thing Supported Channel Groups
+
+| channel group ID |
+|-----------------------------------------------|
+| [Controller](#Cloud-Controller-Channel-Group) |
+| [Zones](#Zone-Channel-Group) |
+| [All Zones](#All-Zones-Channel-Group) |
+| [Sensor](#Sensor-Channel-Group) |
+| [Forecast](#Sensor-Channel-Group) |
### Local Thing
This provides a subset of features compared to the Cloud Thing type limited to basic zone control.
Controlling zones through the local API will not be reported back to the cloud service or the Hydrawise mobile/web applications, and reporting functionality will not reflect the locally controlled state.
+Local control may not be available on later Hydrawise controller firmware versions.
+
Use Cases
* The Local thing can be useful when testing zones, as there is no delay when starting/stopping zones as compared to the cloud API which can take anywhere between 5-15 seconds.
## Thing Configuration
-### Cloud Thing
-
-| Configuration Name | type | required | Comments |
-|--------------------|---------|----------|------------------------------------------------------------------------------------|
-| apiKey | String | True | |
-| refresh | Integer | True | Defaults to a 30 seconds polling rate |
-| controllerId | Integer | False | Optional id of the controller if you have more then one registered to your account |
+### Account Thing
-To obtain your API key, log into your [Hydrawsie Account](https://app.hydrawise.com/config/login) and click on your account icon, then account details:
+| Configuration Name | type | required | Comments |
+|--------------------|---------|----------|--------------------------------------------------------------------------------------------------------------------------|
+| userName | String | False | The Hydrawise account user name |
+| password | String | False | The Hydrawise account password |
+| savePassword | Boolean | False | By default the password will be not be persisted after the first login attempt unless this is true, defaults to false |
+| refresh | Integer | False | Defaults to a 60 second polling rate, more frequent polling may cause the service to deny requests |
+| refreshToken | Boolean | False | A oAuth refresh token, this will be automatically configured after the first login and updated as the token is refreshed |
-
+### Controller Thing
-Then copy the API key shown here:
+| Configuration Name | type | required | Comments |
+|--------------------|---------|----------|----------------------|
+| controllerId | Integer | True | ID of the controller |
-
### Local Thing
| Configuration Name | type | required | Comments |
|--------------------|---------|----------|-----------------------------------------------------------------------------------------------------------------|
| host | String | True | IP or host name of the controller on your network |
-| username | String | True | User name (usually admin) set on the touch panel of the controller |
+| username | String | True | User name (usually admin) set on the touch panel of the controller |
| password | String | True | Password set on the touch panel of the controller. This can be found under the setting menu on the controller. |
| refresh | Integer | True | Defaults to a 30 seconds polling rate |
### Channel Groups
+#### System Channel Group
+
+| channel group ID | Description |
+|------------------|---------------------------------|
+| system | System status of the controller |
+
#### Zone Channel Group
Up to 36 total zones are supported per Local or Cloud thing
#### Forecast Channel Group
-Up to 4 total weather forecasts are supported per Cloud Thing
+Up to 3 total weather forecasts are supported per Cloud Thing
| channel group ID | Description |
|------------------|-----------------|
| forecast1 | Todays Forecast |
| forecast2 | Day 2 Forecast |
| forecast3 | Day 3 Forecast |
-| forecast4 | Day 4 Forecast |
#### All Zones Channel Group
### Channels
-| channel ID | type | Groups | description | Read Write |
-|-----------------|--------------------|----------------|---------------------------------------------|------------|
-| name | String | zone, sensor | Descriptive name | R |
-| icon | String | zone | Icon URL | R |
-| time | Number | zone | Zone start time in seconds | R |
-| type | Number | zone | Zone type | R |
-| runcustom | Number | zone, allzones | Run zone for custom number of seconds | W |
-| run | Switch | zone, allzones | Run/Start zone | RW |
-| nextrun | DateTime | zone | Next date and time this zone will run | R |
-| timeleft | Number | zone | Amount of seconds left for the running zone | R |
-| input | Number | sensor | Sensor input type | R |
-| mode | Number | sensor | Sensor mode | R |
-| timer | Number | sensor | Sensor timer | R |
-| offtimer | Number | sensor | Sensor off time | R |
-| offlevel | Number | sensor | Sensor off level | R |
-| active | Switch | sensor | Is sensor active / triggered | R |
-| temperaturehigh | Number:Temperature | forecast | Daily high temperature | R |
-| temperaturelow | Number:Temperature | forecast | Daily low temperature | R |
-| conditions | String | forecast | Daily conditions description | R |
-| day | String | forecast | Day of week of forecast (Mon-Sun) | R |
-| humidity | Number | forecast | Daily humidity percentage | R |
-| wind | Number:Speed | forecast | Daily wind speed | R |
+Channels uses across zones, sensors and forecasts
+| channel ID | type | Groups | description | Read Write |
+|----------------------------|--------------------|----------------|-----------------------------------------------|------------|
+| name | String | zone, sensor | Descriptive name | R |
+| icon | String | zone | Icon URL | R |
+| type | Number | zone | Zone type | R |
+| run | Switch | zone, allzones | Run/Start zone | RW |
+| runcustom | Number:Time | zone, allzones | Run zone for custom length | W |
+| suspend | Switch | zone, allzones | Suspend zone | RW |
+| suspenduntil | DateTime | zone, allzones | Suspend zone unitl specified date | RW |
+| nextrun | DateTime | zone | Next date and time this zone will run | R |
+| timeleft | Number:Time | zone | Amount of time left for the running zone | R |
+| input | Number | sensor | Sensor input type | R |
+| timer | Number | sensor | Sensor timer | R |
+| offtimer | Number:Time | sensor | Sensor off timer | R |
+| offlevel | Number | sensor | Sensor off level | R |
+| active | Switch | sensor | Is sensor active / triggered | R |
+| temperaturehigh | Number:Temperature | forecast | Daily high temperature | R |
+| temperaturelow | Number:Temperature | forecast | Daily low temperature | R |
+| conditions | String | forecast | Daily conditions description | R |
+| day | DateTime | forecast | Day of week of forecast (Mon-Sun) | R |
+| humidity | Number | forecast | Daily humidity percentage | R |
+| wind | Number:Speed | forecast | Daily wind speed | R |
+| evapotranspiration | Number | forecast | Daily evapotranspiration amount | R |
+| precipitation | Number | forecast | Daily precipitation amount | R |
+| probabilityofprecipitation | Number | forecast | Daily probability of precipitation percentage | R |
+
## Full Example
```
-Group SprinklerZones
+Group Sprinkler "Sprinkler"
+Group SprinklerController "Controller" (Sprinkler)
+Group SprinklerZones "Zones" (Sprinkler)
+Group SprinklerSensors "Sensors" (Sprinkler)
+Group SprinkerForecast "Forecast" (Sprinkler)
+
+String SprinkerControllerStatus "Status [%s]" (SprinklerController) {channel="hydrawise:controller:myaccount:123456:controller#status"}
+Number SprinkerControllerLastContact "Last Contact [%d]" (SprinklerController) {channel="hydrawise:controller:myaccount:123456:controller#lastContact"}
+
+Switch SprinklerSensor1 "Sprinler Sensor" (SprinklerSensors) {channel="hydrawise:controller:myaccount:123456:sensor1#active"}
+
+Group SprinkerForecastDay1 "Todays Forecast" (SprinkerForecast)
+Number:Temperature SprinkerForecastDay1HiTemp "High Temp [%d]" (SprinkerForecastDay1) {channel="hydrawise:controller:myaccount:123456:forecast1#temperaturehigh"}
+Number:Temperature SprinkerForecastDay1LowTemp "Low Temp [%d]" (SprinkerForecastDay1) {channel="hydrawise:controller:myaccount:123456:forecast1#temperaturelow"}
+String SprinkerForecastDay1Conditions "Conditions [%s]" (SprinkerForecastDay1) {channel="hydrawise:controller:myaccount:123456:forecast1#conditions"}
+String SprinkerForecastDay1Day "Day [%s]" (SprinkerForecastDay1) {channel="hydrawise:controller:myaccount:123456:forecast1#day"}
+Number SprinkerForecastDay1Humidity "Humidity [%d%%]" (SprinkerForecastDay1) {channel="hydrawise:controller:myaccount:123456:forecast1#humidity"}
+Number:Speed SprinkerForecastDay1Wind "Wind [%s]" (SprinkerForecastDay1) {channel="hydrawise:controller:myaccount:123456:forecast1#wind"}
+
Group SprinklerZone1 "1 Front Office Yard" (SprinklerZones)
-String SprinklerZone1Name "1 Front Office Yard name" (SprinklerZone1) {channel="hydrawise:cloud:home:zone1#name"}
-Switch SprinklerZone1Run "1 Front Office Yard Run" (SprinklerZone1) {channel="hydrawise:cloud:home:zone1#run"}
+String SprinklerZone1Name "1 Front Office Yard name" (SprinklerZone1) {channel="hydrawise:controller:myaccount:123456:zone1#name"}
+Switch SprinklerZone1Run "1 Front Office Yard Run" (SprinklerZone1) {channel="hydrawise:controller:myaccount:123456:zone1#run"}
Switch SprinklerZone1RunLocal "1 Front Office Yard Run (local)" (SprinklerZone1) {channel="hydrawise:local:home:zone1#run"}
-Number SprinklerZone1RunCustom "1 Front Office Yard Run Custom" (SprinklerZone1) {channel="hydrawise:cloud:home:zone1#runcustom"}
-DateTime SprinklerZone1StartTime "1 Front Office Yard Start Time [%s]" (SprinklerZone1) {channel="hydrawise:cloud:home:zone1#nextruntime"}
-Number SprinklerZone1TimeLeft "1 Front Office Yard Time Left" (SprinklerZone1) {channel="hydrawise:cloud:home:zone1#timeleft"}
-String SprinklerZone1Icon "1 Front Office Yard Icon" (SprinklerZone1) {channel="hydrawise:cloud:home:zone1#icon"}
+Number SprinklerZone1RunCustom "1 Front Office Yard Run Custom" (SprinklerZone1) {channel="hydrawise:controller:myaccount:123456:zone1#runcustom"}
+DateTime SprinklerZone1StartTime "1 Front Office Yard Start Time [%s]" (SprinklerZone1) {channel="hydrawise:controller:myaccount:123456:zone1#nextruntime"}
+Number SprinklerZone1TimeLeft "1 Front Office Yard Time Left" (SprinklerZone1) {channel="hydrawise:controller:myaccount:123456:zone1#timeleft"}
+String SprinklerZone1Icon "1 Front Office Yard Icon" (SprinklerZone1) {channel="hydrawise:controller:myaccount:123456:zone1#icon"}
Group SprinklerZone2 "2 Back Circle Lawn" (SprinklerZones)
-String SprinklerZone2Name "2 Back Circle Lawn name" (SprinklerZone2) {channel="hydrawise:cloud:home:zone2#name"}
-Switch SprinklerZone2Run "2 Back Circle Lawn Run" (SprinklerZone2) {channel="hydrawise:cloud:home:zone2#run"}
+String SprinklerZone2Name "2 Back Circle Lawn name" (SprinklerZone2) {channel="hydrawise:controller:myaccount:123456:zone2#name"}
+Switch SprinklerZone2Run "2 Back Circle Lawn Run" (SprinklerZone2) {channel="hydrawise:controller:myaccount:123456:zone2#run"}
Switch SprinklerZone2RunLocal "2 Back Circle Lawn Run (local)" (SprinklerZone2) {channel="hydrawise:local:home:zone2#run"}
-Number SprinklerZone2RunCustom "2 Back Circle Lawn Run Custom" (SprinklerZone2) {channel="hydrawise:cloud:home:zone2#runcustom"}
-DateTime SprinklerZone2StartTime "2 Back Circle Lawn Start Time" (SprinklerZone2) {channel="hydrawise:cloud:home:zone2#nextruntime"}
-Number SprinklerZone2TimeLeft "2 Back Circle Lawn Time Left" (SprinklerZone2) {channel="hydrawise:cloud:home:zone2#timeleft"}
-String SprinklerZone2Icon "2 Back Circle Lawn Icon" (SprinklerZone2) {channel="hydrawise:cloud:home:zone2#icon"}
+Number SprinklerZone2RunCustom "2 Back Circle Lawn Run Custom" (SprinklerZone2) {channel="hydrawise:controller:myaccount:123456:zone2#runcustom"}
+DateTime SprinklerZone2StartTime "2 Back Circle Lawn Start Time" (SprinklerZone2) {channel="hydrawise:controller:myaccount:123456:zone2#nextruntime"}
+Number SprinklerZone2TimeLeft "2 Back Circle Lawn Time Left" (SprinklerZone2) {channel="hydrawise:controller:myaccount:123456:zone2#timeleft"}
+String SprinklerZone2Icon "2 Back Circle Lawn Icon" (SprinklerZone2) {channel="hydrawise:controller:myaccount:123456:zone2#icon"}
Group SprinklerZone3 "3 Left of Drive Lawn" (SprinklerZones)
-String SprinklerZone3Name "3 Left of Drive Lawn name" (SprinklerZone3) {channel="hydrawise:cloud:home:zone3#name"}
-Switch SprinklerZone3Run "3 Left of Drive Lawn Run" (SprinklerZone3) {channel="hydrawise:cloud:home:zone3#run"}
+String SprinklerZone3Name "3 Left of Drive Lawn name" (SprinklerZone3) {channel="hydrawise:controller:myaccount:123456:zone3#name"}
+Switch SprinklerZone3Run "3 Left of Drive Lawn Run" (SprinklerZone3) {channel="hydrawise:controller:myaccount:123456:zone3#run"}
Switch SprinklerZone3RunLocal "3 Left of Drive Lawn Run (local)" (SprinklerZone3) {channel="hydrawise:local:home:zone3#run"}
-Number SprinklerZone3RunCustom "3 Left of Drive Lawn Run Custom" (SprinklerZone3) {channel="hydrawise:cloud:home:zone3#runcustom"}
-DateTime SprinklerZone3StartTime "3 Left of Drive Lawn Start Time" (SprinklerZone3) {channel="hydrawise:cloud:home:zone3#nextruntime"}
-Number SprinklerZone3TimeLeft "3 Left of Drive Lawn Time Left" (SprinklerZone3) {channel="hydrawise:cloud:home:zone3#timeleft"}
-String SprinklerZone3Icon "3 Left of Drive Lawn Icon" (SprinklerZone3) {channel="hydrawise:cloud:home:zone3#icon"}
+Number SprinklerZone3RunCustom "3 Left of Drive Lawn Run Custom" (SprinklerZone3) {channel="hydrawise:controller:myaccount:123456:zone3#runcustom"}
+DateTime SprinklerZone3StartTime "3 Left of Drive Lawn Start Time" (SprinklerZone3) {channel="hydrawise:controller:myaccount:123456:zone3#nextruntime"}
+Number SprinklerZone3TimeLeft "3 Left of Drive Lawn Time Left" (SprinklerZone3) {channel="hydrawise:controller:myaccount:123456:zone3#timeleft"}
+String SprinklerZone3Icon "3 Left of Drive Lawn Icon" (SprinklerZone3) {channel="hydrawise:controller:myaccount:123456:zone3#icon"}
```
*/
@NonNullByDefault
public class HydrawiseBindingConstants {
-
private static final String BINDING_ID = "hydrawise";
// List of all Thing Type UIDs
- public static final ThingTypeUID THING_TYPE_CLOUD = new ThingTypeUID(BINDING_ID, "cloud");
+ public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
+ public static final ThingTypeUID THING_TYPE_CONTROLLER = new ThingTypeUID(BINDING_ID, "controller");
public static final ThingTypeUID THING_TYPE_LOCAL = new ThingTypeUID(BINDING_ID, "local");
-
public static final String BASE_IMAGE_URL = "https://app.hydrawise.com/config/images/";
+ public static final String CONFIG_USERNAME = "userName";
+ public static final String CONFIG_PASSWORD = "password";
+ public static final String CONFIG_REFRESHTOKEN = "refreshToken";
+ public static final String CONFIG_CONTROLLER_ID = "controllerId";
+
+ public static final String CHANNEL_GROUP_CONTROLLER_SYSTEM = "system";
+ public static final String CHANNEL_CONTROLLER_NAME = "name";
+ public static final String CHANNEL_CONTROLLER_LAST_CONTACT = "lastcontact";
+ public static final String CHANNEL_CONTROLLER_STATUS = "status";
+ public static final String CHANNEL_CONTROLLER_SUMMARY = "summary";
+ public static final String CHANNEL_CONTROLLER_ONLINE = "online";
public static final String CHANNEL_GROUP_ALLZONES = "allzones";
- public static final String CHANNEL_ZONE_RUN_CUSTOM = "runcustom";
- public static final String CHANNEL_ZONE_RUN = "run";
- public static final String CHANNEL_ZONE_STOP = "stop";
- public static final String CHANNEL_ZONE_SUSPEND = "suspend";
public static final String CHANNEL_ZONE_NAME = "name";
public static final String CHANNEL_ZONE_ICON = "icon";
- public static final String CHANNEL_ZONE_LAST_WATER = "lastwater";
- public static final String CHANNEL_ZONE_TIME = "time";
+ public static final String CHANNEL_ZONE_STARTTIME = "starttime";
+ public static final String CHANNEL_ZONE_DURATION = "duration";
public static final String CHANNEL_ZONE_TYPE = "type";
+ public static final String CHANNEL_ZONE_RUN = "run";
+ public static final String CHANNEL_ZONE_RUN_CUSTOM = "runcustom";
public static final String CHANNEL_ZONE_NEXT_RUN_TIME_TIME = "nextruntime";
+ public static final String CHANNEL_ZONE_SUSPEND = "suspend";
+ public static final String CHANNEL_ZONE_SUSPENDUNTIL = "suspenduntil";
+ public static final String CHANNEL_ZONE_SUMMARY = "summary";
public static final String CHANNEL_ZONE_TIME_LEFT = "timeleft";
- public static final String CHANNEL_RUN_ALL_ZONES = "runall";
- public static final String CHANNEL_STOP_ALL_ZONES = "stopall";
- public static final String CHANNEL_SUSPEND_ALL_ZONES = "suspendall";
public static final String CHANNEL_SENSOR_NAME = "name";
public static final String CHANNEL_SENSOR_INPUT = "input";
public static final String CHANNEL_SENSOR_MODE = "mode";
- public static final String CHANNEL_SENSOR_TIMER = "timer";
+ public static final String CHANNEL_SENSOR_DELAY = "delay";
public static final String CHANNEL_SENSOR_OFFTIMER = "offtimer";
public static final String CHANNEL_SENSOR_OFFLEVEL = "offlevel";
public static final String CHANNEL_SENSOR_ACTIVE = "active";
+ public static final String CHANNEL_SENSOR_WATERFLOW = "waterflow";
public static final String CHANNEL_FORECAST_TEMPERATURE_HIGH = "temperaturehigh";
public static final String CHANNEL_FORECAST_TEMPERATURE_LOW = "temperaturelow";
public static final String CHANNEL_FORECAST_CONDITIONS = "conditions";
- public static final String CHANNEL_FORECAST_DAY = "day";
+ public static final String CHANNEL_FORECAST_TIME = "time";
public static final String CHANNEL_FORECAST_HUMIDITY = "humidity";
public static final String CHANNEL_FORECAST_WIND = "wind";
public static final String CHANNEL_FORECAST_ICON = "icon";
+ public static final String CHANNEL_FORECAST_EVAPOTRANSPRIATION = "evapotranspiration";
+ public static final String CHANNEL_FORECAST_PRECIPITATION = "precipitation";
+ public static final String CHANNEL_FORECAST_PROBABILITYOFPRECIPITATION = "probabilityofprecipitation";
+
public static final String PROPERTY_CONTROLLER_ID = "controller";
public static final String PROPERTY_NAME = "name";
public static final String PROPERTY_DESCRIPTION = "description";
+++ /dev/null
-/**
- * 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.hydrawise.internal;
-
-/**
- * The {@link HydrawiseCloudConfiguration} class contains fields mapping thing configuration parameters.
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class HydrawiseCloudConfiguration {
-
- /**
- * Customer API key {@link https://app.hydrawise.com/config/account}
- */
- public String apiKey;
-
- /**
- * refresh interval in seconds.
- */
- public Integer refresh;
-
- /**
- * optional id of the controller to connect to
- */
- public Integer controllerId;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal;
-
-import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
-
-import java.util.List;
-import java.util.Optional;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.eclipse.jetty.client.HttpClient;
-import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
-import org.openhab.binding.hydrawise.internal.api.HydrawiseCloudApiClient;
-import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
-import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
-import org.openhab.binding.hydrawise.internal.api.model.Controller;
-import org.openhab.binding.hydrawise.internal.api.model.CustomerDetailsResponse;
-import org.openhab.binding.hydrawise.internal.api.model.Forecast;
-import org.openhab.binding.hydrawise.internal.api.model.Relay;
-import org.openhab.binding.hydrawise.internal.api.model.StatusScheduleResponse;
-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.ImperialUnits;
-import org.openhab.core.library.unit.SIUnits;
-import org.openhab.core.thing.Thing;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link HydrawiseCloudHandler} is responsible for handling commands, which are
- * sent to one of the channels.
- *
- * @author Dan Cunningham - Initial contribution
- */
-@NonNullByDefault
-public class HydrawiseCloudHandler extends HydrawiseHandler {
- /**
- * 74.2 F
- */
- private static final Pattern TEMPERATURE_PATTERN = Pattern.compile("^(\\d{1,3}.?\\d?)\\s([C,F])");
- /**
- * 9 mph
- */
- private static final Pattern WIND_SPEED_PATTERN = Pattern.compile("^(\\d{1,3})\\s([a-z]{3})");
- private final Logger logger = LoggerFactory.getLogger(HydrawiseCloudHandler.class);
- private HydrawiseCloudApiClient client;
- private int controllerId;
-
- public HydrawiseCloudHandler(Thing thing, HttpClient httpClient) {
- super(thing);
- this.client = new HydrawiseCloudApiClient(httpClient);
- }
-
- @Override
- protected void configure()
- throws NotConfiguredException, HydrawiseConnectionException, HydrawiseAuthenticationException {
- HydrawiseCloudConfiguration configuration = getConfig().as(HydrawiseCloudConfiguration.class);
-
- this.refresh = Math.max(configuration.refresh, MIN_REFRESH_SECONDS);
-
- client.setApiKey(configuration.apiKey);
-
- CustomerDetailsResponse customerDetails = client.getCustomerDetails();
-
- List<Controller> controllers = customerDetails.controllers;
- if (controllers.isEmpty()) {
- throw new NotConfiguredException("No controllers found on account");
- }
-
- Controller controller = null;
- // try and use ID from user configuration
- if (configuration.controllerId != null) {
- controller = getController(configuration.controllerId.intValue(), controllers);
- if (controller == null) {
- throw new NotConfiguredException("No controller found for id " + configuration.controllerId);
- }
- } else {
- // try and use ID from saved property
- String controllerId = getThing().getProperties().get(PROPERTY_CONTROLLER_ID);
- if (controllerId != null && !controllerId.isBlank()) {
- try {
- controller = getController(Integer.parseInt(controllerId), controllers);
- } catch (NumberFormatException e) {
- logger.debug("Can not parse property vaue {}", controllerId);
- }
- }
- // use current controller ID
- if (controller == null) {
- controller = getController(customerDetails.controllerId, controllers);
- }
- }
-
- if (controller == null) {
- throw new NotConfiguredException("No controller found");
- }
-
- controllerId = controller.controllerId.intValue();
- updateControllerProperties(controller);
- logger.debug("Controller id {}", controllerId);
- }
-
- /**
- * Poll the controller for updates.
- */
- @Override
- protected void pollController() throws HydrawiseConnectionException, HydrawiseAuthenticationException {
- List<Controller> controllers = client.getCustomerDetails().controllers;
- Controller controller = getController(controllerId, controllers);
- if (controller != null && !controller.online) {
- throw new HydrawiseConnectionException("Controller is offline");
- }
- StatusScheduleResponse status = client.getStatusSchedule(controllerId);
- updateSensors(status);
- updateForecast(status);
- updateZones(status);
- }
-
- @Override
- protected void sendRunCommand(int seconds, @Nullable Relay relay)
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
- if (relay != null) {
- client.runRelay(seconds, relay.relayId);
- }
- }
-
- @Override
- protected void sendRunCommand(@Nullable Relay relay)
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
- if (relay != null) {
- client.runRelay(relay.relayId);
- }
- }
-
- @Override
- protected void sendStopCommand(@Nullable Relay relay)
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
- if (relay != null) {
- client.stopRelay(relay.relayId);
- }
- }
-
- @Override
- protected void sendRunAllCommand()
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
- client.runAllRelays(controllerId);
- }
-
- @Override
- protected void sendRunAllCommand(int seconds)
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
- client.runAllRelays(seconds, controllerId);
- }
-
- @Override
- protected void sendStopAllCommand()
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
- client.stopAllRelays(controllerId);
- }
-
- private void updateSensors(StatusScheduleResponse status) {
- status.sensors.forEach(sensor -> {
- String group = "sensor" + sensor.input;
- updateGroupState(group, CHANNEL_SENSOR_MODE, new DecimalType(sensor.type));
- updateGroupState(group, CHANNEL_SENSOR_NAME, new StringType(sensor.name));
- updateGroupState(group, CHANNEL_SENSOR_OFFTIMER, new DecimalType(sensor.offtimer));
- updateGroupState(group, CHANNEL_SENSOR_TIMER, new DecimalType(sensor.timer));
- // Some fields are missing depending on sensor type.
- if (sensor.offlevel != null) {
- updateGroupState(group, CHANNEL_SENSOR_OFFLEVEL, new DecimalType(sensor.offlevel));
- }
- if (sensor.active != null) {
- updateGroupState(group, CHANNEL_SENSOR_ACTIVE, sensor.active > 0 ? OnOffType.ON : OnOffType.OFF);
- }
- });
- }
-
- private void updateForecast(StatusScheduleResponse status) {
- int i = 1;
- for (Forecast forecast : status.forecast) {
- String group = "forecast" + (i++);
- updateGroupState(group, CHANNEL_FORECAST_CONDITIONS, new StringType(forecast.conditions));
- updateGroupState(group, CHANNEL_FORECAST_DAY, new StringType(forecast.day));
- updateGroupState(group, CHANNEL_FORECAST_HUMIDITY, new DecimalType(forecast.humidity));
- updateTemperature(forecast.tempHi, group, CHANNEL_FORECAST_TEMPERATURE_HIGH);
- updateTemperature(forecast.tempLo, group, CHANNEL_FORECAST_TEMPERATURE_LOW);
- updateWindspeed(forecast.wind, group, CHANNEL_FORECAST_WIND);
- }
- }
-
- private void updateTemperature(String tempString, String group, String channel) {
- Matcher matcher = TEMPERATURE_PATTERN.matcher(tempString);
- if (matcher.matches()) {
- try {
- updateGroupState(group, channel, new QuantityType<>(Double.valueOf(matcher.group(1)),
- "C".equals(matcher.group(2)) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT));
- } catch (NumberFormatException e) {
- logger.debug("Could not parse temperature string {} ", tempString);
- }
- }
- }
-
- private void updateWindspeed(String windString, String group, String channel) {
- Matcher matcher = WIND_SPEED_PATTERN.matcher(windString);
- if (matcher.matches()) {
- try {
- updateGroupState(group, channel, new QuantityType<>(Integer.parseInt(matcher.group(1)),
- "kph".equals(matcher.group(2)) ? SIUnits.KILOMETRE_PER_HOUR : ImperialUnits.MILES_PER_HOUR));
- } catch (NumberFormatException e) {
- logger.debug("Could not parse wind string {} ", windString);
- }
- }
- }
-
- private void updateControllerProperties(Controller controller) {
- getThing().setProperty(PROPERTY_CONTROLLER_ID, String.valueOf(controller.controllerId));
- getThing().setProperty(PROPERTY_NAME, controller.name);
- getThing().setProperty(PROPERTY_DESCRIPTION, controller.description);
- getThing().setProperty(PROPERTY_LOCATION, controller.latitude + "," + controller.longitude);
- getThing().setProperty(PROPERTY_ADDRESS, controller.address);
- }
-
- private @Nullable Controller getController(int controllerId, List<Controller> controllers) {
- Optional<@NonNull Controller> optionalController = controllers.stream()
- .filter(c -> controllerId == c.controllerId.intValue()).findAny();
- return optionalController.isPresent() ? optionalController.get() : null;
- }
-}
--- /dev/null
+/**
+ * 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.hydrawise.internal;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.Controller;
+
+/**
+ *
+ * @author Dan Cunningham - Initial contribution
+ *
+ */
+@NonNullByDefault
+public interface HydrawiseControllerListener {
+ public void onData(List<Controller> controllers);
+}
+++ /dev/null
-/**
- * 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.hydrawise.internal;
-
-import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
-
-import java.time.ZonedDateTime;
-import java.time.temporal.ChronoUnit;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-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.hydrawise.internal.api.HydrawiseAuthenticationException;
-import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
-import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
-import org.openhab.binding.hydrawise.internal.api.model.LocalScheduleResponse;
-import org.openhab.binding.hydrawise.internal.api.model.Relay;
-import org.openhab.binding.hydrawise.internal.api.model.Running;
-import org.openhab.core.library.types.DateTimeType;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.StringType;
-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.binding.BaseThingHandler;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.openhab.core.types.State;
-import org.openhab.core.types.UnDefType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link HydrawiseHandler} is responsible for handling commands, which are
- * sent to one of the channels.
- *
- * @author Dan Cunningham - Initial contribution
- */
-@NonNullByDefault
-public abstract class HydrawiseHandler extends BaseThingHandler {
-
- private final Logger logger = LoggerFactory.getLogger(HydrawiseHandler.class);
- private @Nullable ScheduledFuture<?> pollFuture;
- private Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<>());
- private Map<String, Relay> relayMap = Collections.synchronizedMap(new HashMap<>());
-
- /**
- * value observed being used by the Hydrawise clients as a max time value,
- */
- private static long MAX_RUN_TIME = 157680000;
-
- /**
- * Minimum amount of time we can poll for updates
- */
- protected static final int MIN_REFRESH_SECONDS = 5;
-
- /**
- * Minimum amount of time we can poll after a command
- */
- protected static final int COMMAND_REFRESH_SECONDS = 5;
-
- /**
- * Our poll rate
- */
- protected int refresh;
-
- /**
- * Future to poll for updated
- */
-
- public HydrawiseHandler(Thing thing) {
- super(thing);
- }
-
- @SuppressWarnings({ "null", "unused" }) // compiler does not like relayMap.get can return null
- @Override
- public void handleCommand(ChannelUID channelUID, Command command) {
- if (getThing().getStatus() != ThingStatus.ONLINE) {
- logger.warn("Controller is NOT ONLINE and is not responding to commands");
- return;
- }
-
- // remove our cached state for this, will be safely updated on next poll
- stateMap.remove(channelUID.getAsString());
-
- if (command instanceof RefreshType) {
- // we already removed this from the cache
- return;
- }
-
- String group = channelUID.getGroupId();
- String channelId = channelUID.getIdWithoutGroup();
- boolean allCommand = CHANNEL_GROUP_ALLZONES.equals(group);
-
- Relay relay = relayMap.get(group);
- if (!allCommand && relay == null) {
- logger.debug("Zone not found {}", group);
- return;
- }
-
- try {
- clearPolling();
- switch (channelId) {
- case CHANNEL_ZONE_RUN_CUSTOM:
- if (!(command instanceof DecimalType)) {
- logger.warn("Invalid command type for run custom {}", command.getClass().getName());
- return;
- }
- if (allCommand) {
- sendRunAllCommand(((DecimalType) command).intValue());
- } else {
- Objects.requireNonNull(relay);
- sendRunCommand(((DecimalType) command).intValue(), relay);
- }
- break;
- case CHANNEL_ZONE_RUN:
- if (!(command instanceof OnOffType)) {
- logger.warn("Invalid command type for run {}", command.getClass().getName());
- return;
- }
- if (allCommand) {
- if (command == OnOffType.ON) {
- sendRunAllCommand();
- } else {
- sendStopAllCommand();
- }
- } else {
- Objects.requireNonNull(relay);
- if (command == OnOffType.ON) {
- sendRunCommand(relay);
- } else {
- sendStopCommand(relay);
- }
- }
- break;
- }
- initPolling(COMMAND_REFRESH_SECONDS);
- } catch (HydrawiseCommandException | HydrawiseConnectionException e) {
- logger.debug("Could not issue command", e);
- initPolling(COMMAND_REFRESH_SECONDS);
- } catch (HydrawiseAuthenticationException e) {
- logger.debug("Credentials not valid");
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
- configureInternal();
- }
- }
-
- @Override
- public void initialize() {
- scheduler.schedule(this::configureInternal, 0, TimeUnit.SECONDS);
- }
-
- @Override
- public void dispose() {
- logger.debug("Handler disposed.");
- clearPolling();
- }
-
- @Override
- public void channelLinked(ChannelUID channelUID) {
- // clear our cached value so the new channel gets updated on the next poll
- stateMap.remove(channelUID.getId());
- }
-
- protected abstract void configure()
- throws NotConfiguredException, HydrawiseConnectionException, HydrawiseAuthenticationException;
-
- protected abstract void pollController() throws HydrawiseConnectionException, HydrawiseAuthenticationException;
-
- protected abstract void sendRunCommand(int seconds, Relay relay)
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
-
- protected abstract void sendRunCommand(Relay relay)
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
-
- protected abstract void sendStopCommand(Relay relay)
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
-
- protected abstract void sendRunAllCommand()
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
-
- protected abstract void sendRunAllCommand(int seconds)
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
-
- protected abstract void sendStopAllCommand()
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
-
- protected void updateZones(LocalScheduleResponse status) {
- ZonedDateTime now = ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS);
- status.relays.forEach(r -> {
- String group = "zone" + r.getRelayNumber();
- relayMap.put(group, r);
- logger.trace("Updateing Zone {} {} ", group, r.name);
- updateGroupState(group, CHANNEL_ZONE_NAME, new StringType(r.name));
- updateGroupState(group, CHANNEL_ZONE_TYPE, new DecimalType(r.type));
- updateGroupState(group, CHANNEL_ZONE_TIME,
- r.runTimeSeconds != null ? new DecimalType(r.runTimeSeconds) : UnDefType.UNDEF);
- String icon = r.icon;
- if (icon != null && !icon.isBlank()) {
- updateGroupState(group, CHANNEL_ZONE_ICON, new StringType(BASE_IMAGE_URL + icon));
- }
- if (r.time >= MAX_RUN_TIME) {
- updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME, UnDefType.UNDEF);
- } else {
- updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME,
- new DateTimeType(now.plusSeconds(r.time).truncatedTo(ChronoUnit.MINUTES)));
- }
-
- Optional<Running> running = status.running.stream()
- .filter(z -> Integer.parseInt(z.relayId) == r.relayId.intValue()).findAny();
- if (running.isPresent()) {
- updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.ON);
- updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(running.get().timeLeft));
- logger.debug("{} Time Left {}", r.name, running.get().timeLeft);
-
- } else {
- updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.OFF);
- updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(0));
-
- }
-
- updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_RUN,
- !status.running.isEmpty() ? OnOffType.ON : OnOffType.OFF);
- });
- }
-
- protected void updateGroupState(String group, String channelID, State state) {
- String channelName = group + "#" + channelID;
- State oldState = stateMap.put(channelName, state);
- if (!state.equals(oldState)) {
- ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelName);
- logger.debug("updateState updating {} {}", channelUID, state);
- updateState(channelUID, state);
- }
- }
-
- @SuppressWarnings("serial")
- @NonNullByDefault
- protected class NotConfiguredException extends Exception {
- NotConfiguredException(String message) {
- super(message);
- }
- }
-
- private boolean isFutureValid(@Nullable ScheduledFuture<?> future) {
- return future != null && !future.isCancelled();
- }
-
- private void configureInternal() {
- clearPolling();
- stateMap.clear();
- relayMap.clear();
- try {
- configure();
- initPolling(0);
- } catch (NotConfiguredException e) {
- logger.debug("Configuration error {}", e.getMessage());
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
- } catch (HydrawiseConnectionException e) {
- logger.debug("Could not connect to service");
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
- } catch (HydrawiseAuthenticationException e) {
- logger.debug("Credentials not valid");
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
- }
- }
-
- /**
- * Starts/Restarts polling with an initial delay. This allows changes in the poll cycle for when commands are sent
- * and we need to poll sooner then the next refresh cycle.
- */
- private synchronized void initPolling(int initalDelay) {
- clearPolling();
- pollFuture = scheduler.scheduleWithFixedDelay(this::pollControllerInternal, initalDelay, refresh,
- TimeUnit.SECONDS);
- }
-
- /**
- * Stops/clears this thing's polling future
- */
- private void clearPolling() {
- ScheduledFuture<?> localFuture = pollFuture;
- if (isFutureValid(localFuture)) {
- if (localFuture != null) {
- localFuture.cancel(false);
- }
- }
- }
-
- /**
- * Poll the controller for updates.
- */
- private void pollControllerInternal() {
- try {
- pollController();
- if (getThing().getStatus() != ThingStatus.ONLINE) {
- updateStatus(ThingStatus.ONLINE);
- }
- } catch (HydrawiseConnectionException e) {
- // poller will continue to run, set offline until next run
- logger.debug("Exception polling", e);
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
- } catch (HydrawiseAuthenticationException e) {
- // if are creds are not valid, we need to try re authorizing again
- logger.debug("Authorization exception during polling", e);
- configureInternal();
- }
- }
-}
import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.hydrawise.internal.handler.HydrawiseAccountHandler;
+import org.openhab.binding.hydrawise.internal.handler.HydrawiseControllerHandler;
+import org.openhab.binding.hydrawise.internal.handler.HydrawiseLocalHandler;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
@NonNullByDefault
@Component(configurationPid = "binding.hydrawise", service = ThingHandlerFactory.class)
public class HydrawiseHandlerFactory extends BaseThingHandlerFactory {
-
- private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream.of(THING_TYPE_CLOUD, THING_TYPE_LOCAL)
- .collect(Collectors.toSet());
-
- private final HttpClient httpClient;
+ private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT,
+ THING_TYPE_CONTROLLER, THING_TYPE_LOCAL);
+ private HttpClient httpClient;
+ private OAuthFactory oAuthFactory;
@Activate
- public HydrawiseHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
+ public HydrawiseHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
+ final @Reference OAuthFactory oAuthFactory) {
+ this.oAuthFactory = oAuthFactory;
this.httpClient = httpClientFactory.getCommonHttpClient();
}
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
- if (THING_TYPE_CLOUD.equals(thingTypeUID)) {
- return new HydrawiseCloudHandler(thing, httpClient);
+ if (THING_TYPE_ACCOUNT.equals(thingTypeUID)) {
+ return new HydrawiseAccountHandler((Bridge) thing, httpClient, oAuthFactory);
+ }
+
+ if (THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
+ return new HydrawiseControllerHandler(thing);
}
if (THING_TYPE_LOCAL.equals(thingTypeUID)) {
+++ /dev/null
-/**
- * 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.hydrawise.internal;
-
-/**
- * The {@link HydrawiseLocalConfiguration} class contains fields mapping thing configuration parameters.
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class HydrawiseLocalConfiguration {
-
- /**
- * Host or IP for local controller
- */
- public String host;
- /**
- * User name (admin) for local controller
- */
- public String username;
- /**
- * Password for local controller
- */
- public String password;
-
- /**
- * refresh interval in seconds.
- */
- public int refresh;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jetty.client.HttpClient;
-import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
-import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
-import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
-import org.openhab.binding.hydrawise.internal.api.HydrawiseLocalApiClient;
-import org.openhab.binding.hydrawise.internal.api.model.Relay;
-import org.openhab.core.thing.Thing;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link HydrawiseLocalHandler} is responsible for handling commands, which are
- * sent to one of the channels.
- *
- * @author Dan Cunningham - Initial contribution
- */
-@NonNullByDefault
-public class HydrawiseLocalHandler extends HydrawiseHandler {
- private final Logger logger = LoggerFactory.getLogger(HydrawiseLocalHandler.class);
- HydrawiseLocalApiClient client;
-
- public HydrawiseLocalHandler(Thing thing, HttpClient httpClient) {
- super(thing);
- client = new HydrawiseLocalApiClient(httpClient);
- }
-
- @Override
- protected void configure() throws HydrawiseConnectionException, HydrawiseAuthenticationException {
- HydrawiseLocalConfiguration configuration = getConfig().as(HydrawiseLocalConfiguration.class);
- this.refresh = Math.max(configuration.refresh, MIN_REFRESH_SECONDS);
- logger.trace("Connecting to host {}", configuration.host);
- client.setCredentials(configuration.host, configuration.username, configuration.password);
- pollController();
- }
-
- @Override
- protected void pollController() throws HydrawiseConnectionException, HydrawiseAuthenticationException {
- updateZones(client.getLocalSchedule());
- }
-
- @Override
- protected void sendRunCommand(int seconds, Relay relay)
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
- client.runRelay(seconds, relay.relay);
- }
-
- @Override
- protected void sendRunCommand(Relay relay)
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
- client.runRelay(relay.relay);
- }
-
- @Override
- protected void sendStopCommand(Relay relay)
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
- client.stopRelay(relay.relay);
- }
-
- @Override
- protected void sendRunAllCommand()
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
- client.runAllRelays();
- }
-
- @Override
- protected void sendRunAllCommand(int seconds)
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
- client.runAllRelays(seconds);
- }
-
- @Override
- protected void sendStopAllCommand()
- throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
- client.stopAllRelays();
- }
-}
*/
package org.openhab.binding.hydrawise.internal.api;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
/**
- * Thrown when the Hydrawise cloud or local API returns back a "unauthorized" response to commands
- *
+ * Thrown when the Hydrawise API returns back a "unauthorized" response to commands
+ *
* @author Dan Cunningham - Initial contribution
*/
-@SuppressWarnings("serial")
+@NonNullByDefault
public class HydrawiseAuthenticationException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public HydrawiseAuthenticationException() {
+ super();
+ }
+ public HydrawiseAuthenticationException(@Nullable String message) {
+ super(message);
+ }
}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api;
-
-import java.util.Objects;
-import java.util.concurrent.TimeUnit;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jetty.client.HttpClient;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.http.HttpMethod;
-import org.openhab.binding.hydrawise.internal.api.model.CustomerDetailsResponse;
-import org.openhab.binding.hydrawise.internal.api.model.Response;
-import org.openhab.binding.hydrawise.internal.api.model.SetControllerResponse;
-import org.openhab.binding.hydrawise.internal.api.model.SetZoneResponse;
-import org.openhab.binding.hydrawise.internal.api.model.StatusScheduleResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.FieldNamingPolicy;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-
-/**
- * The {@link HydrawiseCloudApiClient} communicates with the cloud based Hydrawise API service
- *
- * @author Dan Cunningham - Initial contribution
- */
-@NonNullByDefault
-public class HydrawiseCloudApiClient {
- private final Logger logger = LoggerFactory.getLogger(HydrawiseCloudApiClient.class);
-
- private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
- .create();
- private static final String BASE_URL = "https://app.hydrawise.com/api/v1/";
- private static final String STATUS_SCHEDUE_URL = BASE_URL
- + "statusschedule.php?api_key=%s&controller_id=%d&hours=168";
- private static final String CUSTOMER_DETAILS_URL = BASE_URL + "customerdetails.php?api_key=%s&type=controllers";
- private static final String SET_CONTROLLER_URL = BASE_URL
- + "setcontroller.php?api_key=%s&controller_id=%d&json=true";
- private static final String SET_ZONE_URL = BASE_URL + "setzone.php?period_id=999";
- private static final int TIMEOUT_SECONDS = 30;
- private final HttpClient httpClient;
- private String apiKey;
-
- /**
- * Initializes the API client with a HydraWise API key from a user's account and the HTTPClient to use
- *
- */
- public HydrawiseCloudApiClient(String apiKey, HttpClient httpClient) {
- this.apiKey = apiKey;
- this.httpClient = httpClient;
- }
-
- /**
- * Initializes the API client with a HTTPClient to use
- *
- */
- public HydrawiseCloudApiClient(HttpClient httpClient) {
- this("", httpClient);
- }
-
- /**
- * Set a new API key to use for requests
- *
- * @param apiKey
- */
- public void setApiKey(String apiKey) {
- this.apiKey = apiKey;
- }
-
- /**
- * Retrieves the {@link StatusScheduleResponse} for a given controller
- *
- * @param controllerId
- * @return
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- */
- public StatusScheduleResponse getStatusSchedule(int controllerId)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException {
- String json = doGet(String.format(STATUS_SCHEDUE_URL, apiKey, controllerId));
- StatusScheduleResponse response = Objects.requireNonNull(gson.fromJson(json, StatusScheduleResponse.class));
- throwExceptionIfResponseError(response);
- return response;
- }
-
- /***
- * Retrieves the {@link CustomerDetailsResponse}
- *
- * @return
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- */
- public CustomerDetailsResponse getCustomerDetails()
- throws HydrawiseConnectionException, HydrawiseAuthenticationException {
- String json = doGet(String.format(CUSTOMER_DETAILS_URL, apiKey));
- CustomerDetailsResponse response = Objects.requireNonNull(gson.fromJson(json, CustomerDetailsResponse.class));
- throwExceptionIfResponseError(response);
- return response;
- }
-
- /***
- * Sets the controller with supplied {@param id} as the current controller
- *
- * @param id
- * @return SetControllerResponse
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public SetControllerResponse setController(int id)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- String json = doGet(String.format(SET_CONTROLLER_URL, apiKey, id));
- SetControllerResponse response = Objects.requireNonNull(gson.fromJson(json, SetControllerResponse.class));
- throwExceptionIfResponseError(response);
- if (!response.message.equals("OK")) {
- throw new HydrawiseCommandException(response.message);
- }
- return response;
- }
-
- /***
- * Stops a given relay
- *
- * @param relayId
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String stopRelay(int relayId)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(
- new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("stop").relayId(relayId).toString());
- }
-
- /**
- * Stops all relays on a given controller
- *
- * @param controllerId
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String stopAllRelays(int controllerId)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("stopall")
- .controllerId(controllerId).toString());
- }
-
- /**
- * Runs a relay for the default amount of time
- *
- * @param relayId
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String runRelay(int relayId)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(
- new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("run").relayId(relayId).toString());
- }
-
- /**
- * Runs a relay for the given amount of seconds
- *
- * @param seconds
- * @param relayId
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String runRelay(int seconds, int relayId)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("run").relayId(relayId)
- .duration(seconds).toString());
- }
-
- /**
- * Run all relays on a given controller for the default amount of time
- *
- * @param controllerId
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String runAllRelays(int controllerId)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("runall")
- .controllerId(controllerId).toString());
- }
-
- /***
- * Run all relays on a given controller for the amount of seconds
- *
- * @param seconds
- * @param controllerId
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String runAllRelays(int seconds, int controllerId)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("runall")
- .controllerId(controllerId).duration(seconds).toString());
- }
-
- /**
- * Suspends a given relay
- *
- * @param relayId
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String suspendRelay(int relayId)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(
- new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("suspend").relayId(relayId).toString());
- }
-
- /**
- * Suspends a given relay for an amount of seconds
- *
- * @param seconds
- * @param relayId
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String suspendRelay(int seconds, int relayId)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("suspend").relayId(relayId)
- .duration(seconds).toString());
- }
-
- /**
- * Suspend all relays on a given controller for an amount of seconds
- *
- * @param seconds
- * @param controllerId
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String suspendAllRelays(int seconds, int controllerId)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("suspendall")
- .controllerId(controllerId).duration(seconds).toString());
- }
-
- private String relayCommand(String url)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- String json = doGet(url);
- SetZoneResponse response = Objects.requireNonNull(gson.fromJson(json, SetZoneResponse.class));
- throwExceptionIfResponseError(response);
- if ("error".equals(response.messageType)) {
- throw new HydrawiseCommandException(response.message);
- }
- return response.message;
- }
-
- private String doGet(String url) throws HydrawiseConnectionException {
- logger.trace("Getting {}", url);
- ContentResponse response;
- try {
- response = httpClient.newRequest(url).method(HttpMethod.GET).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
- .send();
- } catch (Exception e) {
- throw new HydrawiseConnectionException(e);
- }
- if (response.getStatus() != 200) {
- throw new HydrawiseConnectionException(
- "Could not connect to Hydrawise API. Response code " + response.getStatus());
- }
- String stringResponse = response.getContentAsString();
- logger.trace("Response: {}", stringResponse);
- return stringResponse;
- }
-
- private void throwExceptionIfResponseError(Response response)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException {
- String error = response.errorMsg;
- if (error != null) {
- if (error.equalsIgnoreCase("unauthorized")) {
- throw new HydrawiseAuthenticationException();
- } else {
- throw new HydrawiseConnectionException(response.errorMsg);
- }
- }
- }
-}
*/
package org.openhab.binding.hydrawise.internal.api;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
/**
* Thrown when command responses return a error message
*
* @author Dan Cunningham - Initial contribution
*/
-@SuppressWarnings("serial")
+@NonNullByDefault
public class HydrawiseCommandException extends Exception {
+ private static final long serialVersionUID = 1L;
+
public HydrawiseCommandException(String message) {
super(message);
}
*/
package org.openhab.binding.hydrawise.internal.api;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
/**
* Thrown for connection issues to the Hydrawise controller
*
* @author Dan Cunningham - Initial contribution
*/
-@SuppressWarnings("serial")
+@NonNullByDefault
public class HydrawiseConnectionException extends Exception {
+ private static final long serialVersionUID = 1L;
public HydrawiseConnectionException(Exception e) {
super(e);
+++ /dev/null
-/**
- * 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.hydrawise.internal.api;
-
-import java.net.URI;
-import java.util.Objects;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jetty.client.HttpClient;
-import org.eclipse.jetty.client.api.AuthenticationStore;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.util.BasicAuthentication;
-import org.eclipse.jetty.http.HttpMethod;
-import org.openhab.binding.hydrawise.internal.api.model.LocalScheduleResponse;
-import org.openhab.binding.hydrawise.internal.api.model.SetZoneResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.FieldNamingPolicy;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-
-/**
- * The {@link HydrawiseLocalApiClient} communicates with a network local Hydrawise controller.
- *
- * @author Dan Cunningham - Initial contribution
- */
-@NonNullByDefault
-public class HydrawiseLocalApiClient {
- private final Logger logger = LoggerFactory.getLogger(HydrawiseLocalApiClient.class);
-
- private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
- .create();
-
- private static final String GET_LOCAL_DATA_URL = "%s/get_sched_json.php?hours=720";
- private static final String SET_LOCAL_DATA_URL = "%s/set_manual_data.php?period_id=998";
-
- private static final int TIMEOUT_SECONDS = 30;
- private HttpClient httpClient;
- private String localSetURL = "";
- private String localGetURL = "";
-
- public HydrawiseLocalApiClient(HttpClient httpClient) {
- this.httpClient = httpClient;
- }
-
- /**
- * Initializes the {@link HydrawiseLocalApiClient} to talk with the network local Hydrawise API
- *
- * @param host
- * @param username
- * @param password
- */
- public HydrawiseLocalApiClient(String host, String username, String password, HttpClient httpClient) {
- this.httpClient = httpClient;
- setCredentials(host, username, password);
- }
-
- /**
- * Sets the local credentials and controller host
- *
- * @param host
- * @param username
- * @param password
- */
- public void setCredentials(String host, String username, String password) {
- String url = "http://" + host;
- localSetURL = String.format(SET_LOCAL_DATA_URL, url);
- localGetURL = String.format(GET_LOCAL_DATA_URL, url);
- AuthenticationStore auth = httpClient.getAuthenticationStore();
- URI uri = URI.create(url);
- auth.addAuthenticationResult(new BasicAuthentication.BasicResult(uri, username, password));
- }
-
- /**
- * Retrieves the {@link LocalScheduleResponse} for the controller
- *
- * @return the local schedule response
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- */
- public LocalScheduleResponse getLocalSchedule()
- throws HydrawiseConnectionException, HydrawiseAuthenticationException {
- String json = doGet(localGetURL);
- LocalScheduleResponse response = gson.fromJson(json, LocalScheduleResponse.class);
- return Objects.requireNonNull(response);
- }
-
- /**
- * Stops a given relay
- *
- * @param number
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String stopRelay(int number)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("stop").relayNumber(number).toString());
- }
-
- /**
- * Runs a given relay for the default amount of time
- *
- * @param number
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String runRelay(int number)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("run").relayNumber(number).toString());
- }
-
- /**
- * Runs a given relay for a specified numbers of seconds
- *
- * @param seconds
- * @param number
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String runRelay(int seconds, int number)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("run").relayNumber(number)
- .duration(seconds).toString());
- }
-
- /**
- * Stops all relays
- *
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String stopAllRelays()
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("stopall").toString());
- }
-
- /**
- * Run all relays for the default amount of time
- *
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String runAllRelays()
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("runall").toString());
- }
-
- /**
- * Run all relays for a given amount of seconds
- *
- * @param seconds
- * @return Response message
- * @throws HydrawiseConnectionException
- * @throws HydrawiseAuthenticationException
- * @throws HydrawiseCommandException
- */
- public String runAllRelays(int seconds)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("runall").duration(seconds).toString());
- }
-
- private String relayCommand(String url)
- throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
- String json = doGet(url);
- SetZoneResponse response = gson.fromJson(json, SetZoneResponse.class);
- if (response.messageType.equals("error")) {
- throw new HydrawiseCommandException(response.message);
- }
- return response.message;
- }
-
- private String doGet(String url) throws HydrawiseConnectionException, HydrawiseAuthenticationException {
- logger.trace("Getting {}", url);
- ContentResponse response;
- try {
- response = httpClient.newRequest(url).method(HttpMethod.GET).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
- .send();
- } catch (InterruptedException | TimeoutException | ExecutionException e) {
- throw new HydrawiseConnectionException(e);
- }
- if (response.getStatus() == 401) {
- throw new HydrawiseAuthenticationException();
- }
- if (response.getStatus() != 200) {
- throw new HydrawiseConnectionException("Error from controller. Response code " + response.getStatus());
- }
- String stringResponse = response.getContentAsString();
- logger.trace("Response: {}", stringResponse);
- return stringResponse;
- }
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api;
-
-/**
- * The {@link HydrawiseZoneCommandBuilder} class builds a command URL string to use when sending commands to the
- * Hydrawise local controller or cloud based API server
- *
- * @author Dan Cunningham - Initial contribution
- *
- */
-class HydrawiseZoneCommandBuilder {
-
- private final StringBuilder builder;
-
- /**
- * Construct a new {@link HydrawiseZoneCommandBuilder} class with a base URL
- *
- * @param baseURL
- */
- public HydrawiseZoneCommandBuilder(String baseURL) {
- builder = new StringBuilder(baseURL);
- }
-
- /**
- * Construct a new {@link HydrawiseZoneCommandBuilder} class with a base URL and API key.
- *
- * @param baseURL
- * @param apiKey
- */
- public HydrawiseZoneCommandBuilder(String baseURL, String apiKey) {
- this(baseURL);
- builder.append("&api_key=" + apiKey);
- }
-
- /**
- * Sets the action parameter
- *
- * @param action
- * @return {@link HydrawiseZoneCommandBuilder}
- */
- public HydrawiseZoneCommandBuilder action(String action) {
- builder.append("&action=" + action);
- return this;
- }
-
- /**
- * Sets the relayId parameter
- *
- * @param action
- * @return {@link HydrawiseZoneCommandBuilder}
- */
- public HydrawiseZoneCommandBuilder relayId(int relayId) {
- builder.append("&relay_id=" + relayId);
- return this;
- }
-
- /**
- * Sets the relay number parameter
- *
- * @param action
- * @return {@link HydrawiseZoneCommandBuilder}
- */
- public HydrawiseZoneCommandBuilder relayNumber(int number) {
- builder.append("&relay=" + number);
- return this;
- }
-
- /**
- * Sets the run duration parameter
- *
- * @param action
- * @return {@link HydrawiseZoneCommandBuilder}
- */
- public HydrawiseZoneCommandBuilder duration(int seconds) {
- builder.append("&custom=" + seconds);
- return this;
- }
-
- /**
- * Sets the controller Id parameter
- *
- * @param action
- * @return {@link HydrawiseZoneCommandBuilder}
- */
- public HydrawiseZoneCommandBuilder controllerId(int controllerId) {
- builder.append("&controller_id=" + controllerId);
- return this;
- }
-
- @Override
- public String toString() {
- return builder.toString();
- }
-}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Type;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+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.Response;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.ControllerStatus;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.Forecast;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.Mutation;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.MutationResponse;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.MutationResponse.MutationResponseStatus;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.MutationResponse.StatusCode;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.QueryRequest;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.QueryResponse;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.ScheduledRuns;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.Sensor;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.Zone;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.ZoneRun;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthException;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+
+/**
+ *
+ * @author Dan Cunningham - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class HydrawiseGraphQLClient {
+ private final Logger logger = LoggerFactory.getLogger(HydrawiseGraphQLClient.class);
+
+ private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .registerTypeAdapter(Zone.class, new ResponseDeserializer<Zone>())
+ .registerTypeAdapter(ScheduledRuns.class, new ResponseDeserializer<ScheduledRuns>())
+ .registerTypeAdapter(ZoneRun.class, new ResponseDeserializer<ZoneRun>())
+ .registerTypeAdapter(Forecast.class, new ResponseDeserializer<Forecast>())
+ .registerTypeAdapter(Sensor.class, new ResponseDeserializer<Forecast>())
+ .registerTypeAdapter(ControllerStatus.class, new ResponseDeserializer<ControllerStatus>()).create();
+
+ private static final String GRAPH_URL = "https://app.hydrawise.com/api/v2/graph";
+ private static final String MUTATION_START_ZONE = "startZone(zoneId: %d) { status }";
+ private static final String MUTATION_START_ZONE_CUSTOM = "startZone(zoneId: %d, customRunDuration: %d) { status }";
+ private static final String MUTATION_START_ALL_ZONES = "startAllZones(controllerId: %d){ status }";
+ private static final String MUTATION_START_ALL_ZONES_CUSTOM = "startAllZones(controllerId: %d, markRunAsScheduled: false, customRunDuration: %d ){ status }";
+ private static final String MUTATION_STOP_ZONE = "stopZone(zoneId: %d) { status }";
+ private static final String MUTATION_STOP_ALL_ZONES = "stopAllZones(controllerId: %d){ status }";
+ private static final String MUTATION_SUSPEND_ZONE = "suspendZone(zoneId: %d, until: \"%s\"){ status }";
+ private static final String MUTATION_SUSPEND_ALL_ZONES = "suspendAllZones(controllerId: %d, until: \"%s\"){ status }";
+ private static final String MUTATION_RESUME_ZONE = "resumeZone(zoneId: %d){ status }";
+ private static final String MUTATION_RESUME_ALL_ZONES = "resumeAllZones(controllerId: %d){ status }";
+
+ private final HttpClient httpClient;
+ private final OAuthClientService oAuthService;
+ private String queryString = "";
+
+ public HydrawiseGraphQLClient(HttpClient httpClient, OAuthClientService oAuthService) {
+ this.httpClient = httpClient;
+ this.oAuthService = oAuthService;
+ }
+
+ /**
+ * Sends a GrapQL query for controller data
+ *
+ * @return QueryResponse
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ */
+ public @Nullable QueryResponse queryControllers()
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException {
+ QueryRequest query;
+ try {
+ query = new QueryRequest(getQueryString());
+ } catch (IOException e) {
+ throw new HydrawiseConnectionException(e);
+ }
+ String queryJson = gson.toJson(query);
+ String response = sendGraphQLQuery(queryJson);
+ return gson.fromJson(response, QueryResponse.class);
+ }
+
+ /***
+ * Stops a given relay
+ *
+ * @param relayId
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public void stopRelay(int relayId)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ sendGraphQLMutation(String.format(MUTATION_STOP_ZONE, relayId));
+ }
+
+ /**
+ * Stops all relays on a given controller
+ *
+ * @param controllerId
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public void stopAllRelays(int controllerId)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ sendGraphQLMutation(String.format(MUTATION_STOP_ALL_ZONES, controllerId));
+ }
+
+ /**
+ * Runs a relay for the default amount of time
+ *
+ * @param relayId
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public void runRelay(int relayId)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ sendGraphQLMutation(String.format(MUTATION_START_ZONE, relayId));
+ }
+
+ /**
+ * Runs a relay for the given amount of seconds
+ *
+ * @param relayId
+ * @param seconds
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public void runRelay(int relayId, int seconds)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ sendGraphQLMutation(String.format(MUTATION_START_ZONE_CUSTOM, relayId, seconds));
+ }
+
+ /**
+ * Run all relays on a given controller for the default amount of time
+ *
+ * @param controllerId
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public void runAllRelays(int controllerId)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ sendGraphQLMutation(String.format(MUTATION_START_ALL_ZONES, controllerId));
+ }
+
+ /***
+ * Run all relays on a given controller for the amount of seconds
+ *
+ * @param controllerId
+ * @param seconds
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public void runAllRelays(int controllerId, int seconds)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ sendGraphQLMutation(String.format(MUTATION_START_ALL_ZONES_CUSTOM, controllerId, seconds));
+ }
+
+ /**
+ * Suspends a given relay
+ *
+ * @param relayId
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public void suspendRelay(int relayId, String until)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ sendGraphQLMutation(String.format(MUTATION_SUSPEND_ZONE, relayId, until));
+ }
+
+ /**
+ * Resumes a given relay
+ *
+ * @param relayId
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public void resumeRelay(int relayId)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ sendGraphQLMutation(String.format(MUTATION_RESUME_ZONE, relayId));
+ }
+
+ /**
+ * Suspend all relays on a given controller for an amount of seconds
+ *
+ * @param controllerId
+ * @param until
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public void suspendAllRelays(int controllerId, String until)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ sendGraphQLMutation(String.format(MUTATION_SUSPEND_ALL_ZONES, controllerId, until));
+ }
+
+ /**
+ * Resumes all relays on a given controller
+ *
+ * @param controllerId
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public void resumeAllRelays(int controllerId)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ sendGraphQLMutation(String.format(MUTATION_RESUME_ALL_ZONES, controllerId));
+ }
+
+ private String sendGraphQLQuery(String content)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException {
+ return sendGraphQLRequest(content);
+ }
+
+ private void sendGraphQLMutation(String content)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ Mutation mutation = new Mutation(content);
+ logger.debug("Sending Mutation {}", gson.toJson(mutation).toString());
+ String response = sendGraphQLRequest(gson.toJson(mutation).toString());
+ logger.debug("Mutation response {}", response);
+ MutationResponse mResponse = gson.fromJson(response, MutationResponse.class);
+ if (mResponse == null) {
+ throw new HydrawiseCommandException("Malformed response: " + response);
+ }
+ Optional<MutationResponseStatus> status = mResponse.data.values().stream().findFirst();
+ if (!status.isPresent()) {
+ throw new HydrawiseCommandException("Unknown response: " + response);
+ }
+ if (status.get().status != StatusCode.OK) {
+ throw new HydrawiseCommandException("Command Status: " + status.get().status.name());
+ }
+ }
+
+ private String sendGraphQLRequest(String content)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException {
+ logger.trace("Sending Request: {}", content);
+ ContentResponse response;
+ final AtomicInteger responseCode = new AtomicInteger(0);
+ final StringBuilder responseMessage = new StringBuilder();
+ try {
+ AccessTokenResponse token = oAuthService.getAccessTokenResponse();
+ if (token == null) {
+ throw new HydrawiseAuthenticationException("Login required");
+ }
+ response = httpClient.newRequest(GRAPH_URL).method(HttpMethod.POST)
+ .content(new StringContentProvider(content), "application/json")
+ .header("Authorization", token.getTokenType() + " " + token.getAccessToken())
+ .onResponseFailure(new Response.FailureListener() {
+ @Override
+ public void onFailure(@Nullable Response response, @Nullable Throwable failure) {
+ int status = response != null ? response.getStatus() : -1;
+ String reason = response != null ? response.getReason() : "Null response";
+ logger.trace("onFailure code: {} message: {}", status, reason);
+ responseCode.set(status);
+ responseMessage.append(reason);
+ }
+ }).send();
+ String stringResponse = response.getContentAsString();
+ logger.trace("Received Response: {}", stringResponse);
+ return stringResponse;
+ } catch (InterruptedException | TimeoutException | OAuthException | IOException e) {
+ logger.debug("Could not send request", e);
+ throw new HydrawiseConnectionException(e);
+ } catch (OAuthResponseException e) {
+ throw new HydrawiseAuthenticationException(e.getMessage());
+ } catch (ExecutionException e) {
+ // Hydrawise returns back a 40x status, but without a valid Realm , so jetty throws an exception,
+ // this allows us to catch this in a callback and handle accordingly
+ switch (responseCode.get()) {
+ case 401:
+ case 403:
+ throw new HydrawiseAuthenticationException(responseMessage.toString());
+ default:
+ throw new HydrawiseConnectionException(e);
+ }
+ }
+ }
+
+ private String getQueryString() throws IOException {
+ if (queryString.isBlank()) {
+ try (InputStream inputStream = HydrawiseGraphQLClient.class.getClassLoader()
+ .getResourceAsStream("query.graphql");
+ BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
+ queryString = bufferedReader.lines().collect(Collectors.joining("\n"));
+ }
+ }
+ return queryString;
+ }
+
+ class ResponseDeserializer<T> implements JsonDeserializer<T> {
+ @Override
+ @Nullable
+ public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException {
+ return new Gson().fromJson(je, type);
+ }
+ }
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class AuthToken {
+ public String tokenType;
+ public Integer expiresIn;
+ public String accessToken;
+ public String refreshToken;
+ public Long issued;
+
+ public AuthToken() {
+ super();
+ issued = System.currentTimeMillis();
+ }
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+import java.util.List;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+
+public class Controller {
+ public Integer id;
+ public String name;
+ public ControllerStatus status;
+ public Location location;
+ public List<Zone> zones = null;
+ public List<Sensor> sensors = null;
+ public List<Forecast> forecast = null;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ *
+ * @author Dan Cunningham - Initial contribution
+ *
+ */
+public class ControllerStatus {
+ public Integer id;
+ public String name;
+ public String summary;
+ public Boolean online;
+ public Time lastContact;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Coordinates {
+ public Double latitude;
+ public Double longitude;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+import java.util.List;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Customer {
+ public String email;
+ public String lastContact;
+ public List<Controller> controllers = null;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Data {
+ public Customer me;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Forecast {
+ public String time;
+ public String updateTime;
+ public String conditions;
+ public UnitValue highTemperature;
+ public UnitValue lowTemperature;
+ public UnitValue evapotranspiration;
+ public Integer probabilityOfPrecipitation;
+ public UnitValue precipitation;
+ public Number averageHumidity;
+ public UnitValue averageWindSpeed;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Icon {
+ public Integer id;
+ public String fileName;
+ public Object customImage;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Input {
+ public Integer number;
+ public String label;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+import java.util.List;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Location {
+ public Coordinates coordinates;
+ public List<Forecast> forecast;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Mutation {
+ private static final String MUTATION_TEMPLATE = "mutation { %s }";
+
+ public String query;
+
+ public Mutation(String graphQLquery) {
+ this.query = String.format(MUTATION_TEMPLATE, graphQLquery);
+ }
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+import java.util.Map;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class MutationResponse {
+ public Map<String, MutationResponseStatus> data;
+
+ public class MutationResponseStatus {
+ public StatusCode status;
+ }
+
+ public enum StatusCode {
+ OK,
+ WARNING,
+ ERROR;
+ }
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class PastRuns {
+ public ZoneRun lastRun;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class QueryRequest {
+ public String query;
+
+ public QueryRequest(String query) {
+ this.query = query;
+ }
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+import java.util.List;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class QueryResponse {
+ public Data data;
+ public List<QueryResponseError> errors;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class QueryResponseError {
+ public String message;
+ public QueryResponseErrorExtensions extentions;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class QueryResponseErrorExtensions {
+ public String category;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class ScheduledRuns {
+ public String summary;
+ public ZoneRun nextRun;
+ public ZoneRun currentRun;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Sensor {
+ public Integer id;
+ public String name;
+ public Input input;
+ public SensorStatus status;
+ public SensorModel model;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class SensorModel {
+ public String modeType;
+ public Boolean active;
+ public Integer offLevel;
+ public Integer offTimer;
+ public Integer delay;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class SensorStatus {
+ public Boolean active;
+ public UnitValue waterFlow;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Time {
+ public Integer timestamp;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class UnitValue {
+ public Number value;
+ public String unit;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Zone {
+ public Integer id;
+ public String name;
+ public ZoneStatus status;
+ public Icon icon;
+ public ZoneNumber number;
+ public ScheduledRuns scheduledRuns;
+ public PastRuns pastRuns;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class ZoneNumber {
+ public Integer value;
+ public String label;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class ZoneRun {
+ public String id;
+ public Time startTime;
+ public Time endTime;
+ public Integer duration;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.graphql.dto;
+
+/**
+ * @author Dan Cunningham - Initial contribution
+ */
+public class ZoneStatus {
+ public Time suspendedUntil;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local;
+
+import java.net.URI;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.AuthenticationStore;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.util.BasicAuthentication;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
+import org.openhab.binding.hydrawise.internal.api.local.dto.LocalScheduleResponse;
+import org.openhab.binding.hydrawise.internal.api.local.dto.SetZoneResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * The {@link HydrawiseLocalApiClient} communicates with a network local Hydrawise controller.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class HydrawiseLocalApiClient {
+ private final Logger logger = LoggerFactory.getLogger(HydrawiseLocalApiClient.class);
+
+ private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .create();
+
+ private static final String GET_LOCAL_DATA_URL = "%s/get_sched_json.php?hours=720";
+ private static final String SET_LOCAL_DATA_URL = "%s/set_manual_data.php?period_id=998";
+
+ private static final int TIMEOUT_SECONDS = 30;
+ private HttpClient httpClient;
+ private String localSetURL = "";
+ private String localGetURL = "";
+
+ public HydrawiseLocalApiClient(HttpClient httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ /**
+ * Initializes the {@link HydrawiseLocalApiClient} to talk with the network local Hydrawise API
+ *
+ * @param host
+ * @param username
+ * @param password
+ */
+ public HydrawiseLocalApiClient(String host, String username, String password, HttpClient httpClient) {
+ this.httpClient = httpClient;
+ setCredentials(host, username, password);
+ }
+
+ /**
+ * Sets the local credentials and controller host
+ *
+ * @param host
+ * @param username
+ * @param password
+ */
+ public void setCredentials(String host, String username, String password) {
+ String url = "http://" + host;
+ localSetURL = String.format(SET_LOCAL_DATA_URL, url);
+ localGetURL = String.format(GET_LOCAL_DATA_URL, url);
+ AuthenticationStore auth = httpClient.getAuthenticationStore();
+ URI uri = URI.create(url);
+ auth.addAuthenticationResult(new BasicAuthentication.BasicResult(uri, username, password));
+ }
+
+ /**
+ * Retrieves the {@link LocalScheduleResponse} for the controller
+ *
+ * @return the local schedule response
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ */
+ @Nullable
+ public LocalScheduleResponse getLocalSchedule()
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException {
+ String json = doGet(localGetURL);
+ LocalScheduleResponse response = gson.fromJson(json, LocalScheduleResponse.class);
+ return response;
+ }
+
+ /**
+ * Stops a given relay
+ *
+ * @param number
+ * @return Response message
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public String stopRelay(int number)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("stop").relayNumber(number).toString());
+ }
+
+ /**
+ * Runs a given relay for the default amount of time
+ *
+ * @param number
+ * @return Response message
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public String runRelay(int number)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("run").relayNumber(number).toString());
+ }
+
+ /**
+ * Runs a given relay for a specified numbers of seconds
+ *
+ * @param seconds
+ * @param number
+ * @return Response message
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public String runRelay(int seconds, int number)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("run").relayNumber(number)
+ .duration(seconds).toString());
+ }
+
+ /**
+ * Stops all relays
+ *
+ * @return Response message
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public String stopAllRelays()
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("stopall").toString());
+ }
+
+ /**
+ * Run all relays for the default amount of time
+ *
+ * @return Response message
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public String runAllRelays()
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("runall").toString());
+ }
+
+ /**
+ * Run all relays for a given amount of seconds
+ *
+ * @param seconds
+ * @return Response message
+ * @throws HydrawiseConnectionException
+ * @throws HydrawiseAuthenticationException
+ * @throws HydrawiseCommandException
+ */
+ public String runAllRelays(int seconds)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("runall").duration(seconds).toString());
+ }
+
+ private String relayCommand(String url)
+ throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+ String json = doGet(url);
+ SetZoneResponse response = gson.fromJson(json, SetZoneResponse.class);
+ if (response.messageType.equals("error")) {
+ throw new HydrawiseCommandException(response.message);
+ }
+ return response.message;
+ }
+
+ private String doGet(String url) throws HydrawiseConnectionException, HydrawiseAuthenticationException {
+ logger.trace("Getting {}", url);
+ ContentResponse response;
+ try {
+ response = httpClient.newRequest(url).method(HttpMethod.GET).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .send();
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ throw new HydrawiseConnectionException(e);
+ }
+ if (response.getStatus() == 401) {
+ throw new HydrawiseAuthenticationException();
+ }
+ if (response.getStatus() != 200) {
+ throw new HydrawiseConnectionException("Error from controller. Response code " + response.getStatus());
+ }
+ String stringResponse = response.getContentAsString();
+ logger.trace("Response: {}", stringResponse);
+ return stringResponse;
+ }
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link HydrawiseZoneCommandBuilder} class builds a command URL string to use when sending commands to the
+ * Hydrawise local controller or cloud based API server
+ *
+ * @author Dan Cunningham - Initial contribution
+ *
+ */
+@NonNullByDefault
+class HydrawiseZoneCommandBuilder {
+
+ private final StringBuilder builder;
+
+ /**
+ * Construct a new {@link HydrawiseZoneCommandBuilder} class with a base URL
+ *
+ * @param baseURL
+ */
+ public HydrawiseZoneCommandBuilder(String baseURL) {
+ builder = new StringBuilder(baseURL);
+ }
+
+ /**
+ * Construct a new {@link HydrawiseZoneCommandBuilder} class with a base URL and API key.
+ *
+ * @param baseURL
+ * @param apiKey
+ */
+ public HydrawiseZoneCommandBuilder(String baseURL, String apiKey) {
+ this(baseURL);
+ builder.append("&api_key=" + apiKey);
+ }
+
+ /**
+ * Sets the action parameter
+ *
+ * @param action
+ * @return {@link HydrawiseZoneCommandBuilder}
+ */
+ public HydrawiseZoneCommandBuilder action(String action) {
+ builder.append("&action=" + action);
+ return this;
+ }
+
+ /**
+ * Sets the relayId parameter
+ *
+ * @param action
+ * @return {@link HydrawiseZoneCommandBuilder}
+ */
+ public HydrawiseZoneCommandBuilder relayId(int relayId) {
+ builder.append("&relay_id=" + relayId);
+ return this;
+ }
+
+ /**
+ * Sets the relay number parameter
+ *
+ * @param action
+ * @return {@link HydrawiseZoneCommandBuilder}
+ */
+ public HydrawiseZoneCommandBuilder relayNumber(int number) {
+ builder.append("&relay=" + number);
+ return this;
+ }
+
+ /**
+ * Sets the run duration parameter
+ *
+ * @param action
+ * @return {@link HydrawiseZoneCommandBuilder}
+ */
+ public HydrawiseZoneCommandBuilder duration(int seconds) {
+ builder.append("&custom=" + seconds);
+ return this;
+ }
+
+ /**
+ * Sets the controller Id parameter
+ *
+ * @param action
+ * @return {@link HydrawiseZoneCommandBuilder}
+ */
+ public HydrawiseZoneCommandBuilder controllerId(int controllerId) {
+ builder.append("&controller_id=" + controllerId);
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return builder.toString();
+ }
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+import java.util.List;
+
+/**
+ * The {@link BocTopologyDesired} class models the actual BocTopology
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class BocTopologyActual {
+
+ public List<Object> bocGateways;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+import java.util.List;
+
+/**
+ * The {@link BocTopologyDesired} class models the desired BocTopology
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class BocTopologyDesired {
+
+ public List<Object> bocGateways;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+import java.util.List;
+
+/**
+ * The {@link Controller} class models a Hydrawise controller unit
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Controller {
+
+ public String name;
+
+ public Integer lastContact;
+
+ public String serialNumber;
+
+ public Integer controllerId;
+
+ public String swVersion;
+
+ public String hardware;
+
+ public Boolean isBoc;
+
+ public String address;
+
+ public String timezone;
+
+ public Integer deviceId;
+
+ public Object parentDeviceId;
+
+ public String image;
+
+ public String description;
+
+ public Integer customerId;
+
+ public Double latitude;
+
+ public Double longitude;
+
+ public String lastContactReadable;
+
+ public String status;
+
+ public String statusIcon;
+
+ public List<String> tags = null;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+import java.util.List;
+
+/**
+ * The {@link CustomerDetailsResponse} class models the CustomerDetails response message
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class CustomerDetailsResponse extends Response {
+
+ public BocTopologyDesired bocTopologyDesired;
+
+ public BocTopologyActual bocTopologyActual;
+
+ public List<Controller> controllers;
+
+ public String currentController;
+
+ public Boolean isBoc;
+
+ public Integer tandc;
+
+ public Integer controllerId;
+
+ public Integer customerId;
+
+ public String sessionId;
+
+ public String hardwareVersion;
+
+ public Integer deviceId;
+
+ public Integer tandcVersion;
+
+ public Features features;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+import java.util.List;
+
+/**
+ * The {@link Features} class models an accounts features.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Features {
+
+ public List<PlanArray> planArray = null;
+
+ public Object id;
+
+ public String planType2;
+
+ public String planType2Key;
+
+ public Object sku;
+
+ public String discount;
+
+ public String cost;
+
+ public String costUs;
+
+ public String costAu;
+
+ public String costEu;
+
+ public String costCa;
+
+ public String costUk;
+
+ public String active;
+
+ public String controllerQty;
+
+ public String rainfall;
+
+ public String smsQty;
+
+ public String scheduledReports;
+
+ public String emailAlerts;
+
+ public String defineSensor;
+
+ public String addUser;
+
+ public String contractor;
+
+ public Object description;
+
+ public String sensorPack;
+
+ public String filelimit;
+
+ public String filetypeall;
+
+ public String planType;
+
+ public String pushNotification;
+
+ public String weatherQty;
+
+ public String weatherFreeQty;
+
+ public String reportingDays;
+
+ public String weatherHourlyUpdates;
+
+ public String freeEnthusiastPlans;
+
+ public String visible;
+
+ public Object contractorPurchasable;
+
+ public Integer boc;
+
+ public Object expiry;
+
+ public Object start;
+
+ public String customerplanId;
+
+ public Integer smsUsed;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+/**
+ * The {@link Forecast} class models a daily weather forecast
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Forecast {
+
+ public String tempHi;
+
+ public String tempLo;
+
+ public String conditions;
+
+ public String day;
+
+ public Integer pop;
+
+ public Integer humidity;
+
+ public String wind;
+
+ public String icon;
+
+ public String iconLocal;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * The {@link LocalScheduleResponse} class models the LocalSchedule response message
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class LocalScheduleResponse extends Response {
+
+ public List<Running> running = new LinkedList<Running>();
+
+ public List<Relay> relays = new LinkedList<Relay>();
+
+ public String name;
+
+ public Integer time;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link PlanArray} class models am account plan.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class PlanArray {
+
+ public String id;
+
+ public Object sku;
+
+ public String discount;
+
+ public String cost;
+
+ public String costUs;
+
+ public String costAu;
+
+ public String costEu;
+
+ public String costCa;
+
+ public String costUk;
+
+ public String active;
+
+ public String controllerQty;
+
+ public String rainfall;
+
+ public String smsQty;
+
+ public String scheduledReports;
+
+ public String emailAlerts;
+
+ public String defineSensor;
+
+ public String addUser;
+
+ public String contractor;
+
+ public String description;
+
+ public String sensorPack;
+
+ public String filelimit;
+
+ public String filetypeall;
+
+ @SerializedName(value = "plan_type")
+ public String planType;
+
+ public String pushNotification;
+
+ public String weatherQty;
+
+ public String weatherFreeQty;
+
+ public String reportingDays;
+
+ public String weatherHourlyUpdates;
+
+ public String freeEnthusiastPlans;
+
+ public String visible;
+
+ public String contractorPurchasable;
+
+ public String boc;
+
+ public String expiry;
+
+ public String start;
+
+ public String customerplanId;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+/**
+ * The {@link Relay} class models the Relay response message
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Relay {
+
+ public Integer relayId;
+
+ public Integer time;
+
+ public Integer type;
+
+ public Integer relay;
+
+ public String name;
+
+ public Integer frequency;
+
+ public String timestr;
+
+ public Integer runSeconds;
+
+ /**
+ * Returns back the actual relay number when multiple controllers are chained.
+ *
+ * @return
+ */
+ public int getRelayNumber() {
+ int quotient = relay / 100;
+ return (relay - (quotient * 100)) + (quotient * 12);
+ }
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+/**
+ * The {@link Response} class models Response messages
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Response {
+
+ public String errorMsg;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+/**
+ * The {@link Running} class models a running relay
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Running {
+
+ public String relay;
+
+ public String relayId;
+
+ public Integer timeLeft;
+
+ public String run;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+import java.util.List;
+
+/**
+ * The {@link Sensor} class models a sensor
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Sensor {
+
+ public Integer input;
+
+ public Integer type;
+
+ public Integer mode;
+
+ public Integer timer;
+
+ public Integer offtimer;
+
+ public String name;
+
+ public Integer offlevel;
+
+ public Integer active;
+
+ public List<Object> relays = null;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+/**
+ * The {@link SetControllerResponse} class models the SetController response message
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class SetControllerResponse extends Response {
+
+ public String name;
+
+ public String controllerId;
+
+ public String message;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+/**
+ * The {@link SetZoneResponse} class models the SetZone response message
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class SetZoneResponse extends Response {
+
+ public String message;
+
+ public String messageType;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.api.local.dto;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * The {@link StatusScheduleResponse} class models the Status and Schedule response message
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class StatusScheduleResponse extends LocalScheduleResponse {
+
+ public Integer controllerId;
+
+ public Integer customerId;
+
+ public Integer userId;
+
+ public Integer nextpoll;
+
+ public List<Sensor> sensors = new LinkedList<Sensor>();
+
+ public String message;
+
+ public String obsRain;
+
+ public String obsRainWeek;
+
+ public String obsMaxtemp;
+
+ public Integer obsRainUpgrade;
+
+ public String obsRainText;
+
+ public String obsCurrenttemp;
+
+ public String wateringTime;
+
+ public Integer waterSaving;
+
+ public String lastContact;
+
+ public List<Forecast> forecast = new LinkedList<Forecast>();
+
+ public String status;
+
+ public String statusIcon;
+}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-import java.util.List;
-
-/**
- * The {@link BocTopologyDesired} class models the actual BocTopology
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class BocTopologyActual {
-
- public List<Object> bocGateways;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-import java.util.List;
-
-/**
- * The {@link BocTopologyDesired} class models the desired BocTopology
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class BocTopologyDesired {
-
- public List<Object> bocGateways;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-import java.util.List;
-
-/**
- * The {@link Controller} class models a Hydrawise controller unit
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class Controller {
-
- public String name;
-
- public Integer lastContact;
-
- public String serialNumber;
-
- public Integer controllerId;
-
- public String swVersion;
-
- public String hardware;
-
- public Boolean isBoc;
-
- public String address;
-
- public String timezone;
-
- public Integer deviceId;
-
- public Object parentDeviceId;
-
- public String image;
-
- public String description;
-
- public Integer customerId;
-
- public Double latitude;
-
- public Double longitude;
-
- public String lastContactReadable;
-
- public String status;
-
- public String statusIcon;
-
- public Boolean online;
-
- public List<String> tags = null;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-import java.util.List;
-
-/**
- * The {@link CustomerDetailsResponse} class models the CustomerDetails response message
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class CustomerDetailsResponse extends Response {
-
- public BocTopologyDesired bocTopologyDesired;
-
- public BocTopologyActual bocTopologyActual;
-
- public List<Controller> controllers;
-
- public String currentController;
-
- public Boolean isBoc;
-
- public Integer tandc;
-
- public Integer controllerId;
-
- public Integer customerId;
-
- public String sessionId;
-
- public String hardwareVersion;
-
- public Integer deviceId;
-
- public Integer tandcVersion;
-
- public Features features;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-import java.util.List;
-
-/**
- * The {@link Features} class models an accounts features.
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class Features {
-
- public List<PlanArray> planArray = null;
-
- public Object id;
-
- public String planType2;
-
- public String planType2Key;
-
- public Object sku;
-
- public String discount;
-
- public String cost;
-
- public String costUs;
-
- public String costAu;
-
- public String costEu;
-
- public String costCa;
-
- public String costUk;
-
- public String active;
-
- public String controllerQty;
-
- public String rainfall;
-
- public String smsQty;
-
- public String scheduledReports;
-
- public String emailAlerts;
-
- public String defineSensor;
-
- public String addUser;
-
- public String contractor;
-
- public Object description;
-
- public String sensorPack;
-
- public String filelimit;
-
- public String filetypeall;
-
- public String planType;
-
- public String pushNotification;
-
- public String weatherQty;
-
- public String weatherFreeQty;
-
- public String reportingDays;
-
- public String weatherHourlyUpdates;
-
- public String freeEnthusiastPlans;
-
- public String visible;
-
- public Object contractorPurchasable;
-
- public Integer boc;
-
- public Object expiry;
-
- public Object start;
-
- public String customerplanId;
-
- public Integer smsUsed;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-/**
- * The {@link Forecast} class models a daily weather forecast
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class Forecast {
-
- public String tempHi;
-
- public String tempLo;
-
- public String conditions;
-
- public String day;
-
- public Integer pop;
-
- public Integer humidity;
-
- public String wind;
-
- public String icon;
-
- public String iconLocal;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-import java.util.LinkedList;
-import java.util.List;
-
-/**
- * The {@link LocalScheduleResponse} class models the LocalSchedule response message
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class LocalScheduleResponse extends Response {
-
- public List<Running> running = new LinkedList<>();
-
- public List<Relay> relays = new LinkedList<>();
-
- public String name;
-
- public Integer time;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-/**
- * The {@link PlanArray} class models am account plan.
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class PlanArray {
-
- public String id;
-
- public Object sku;
-
- public String discount;
-
- public String cost;
-
- public String costUs;
-
- public String costAu;
-
- public String costEu;
-
- public String costCa;
-
- public String costUk;
-
- public String active;
-
- public String controllerQty;
-
- public String rainfall;
-
- public String smsQty;
-
- public String scheduledReports;
-
- public String emailAlerts;
-
- public String defineSensor;
-
- public String addUser;
-
- public String contractor;
-
- public String description;
-
- public String sensorPack;
-
- public String filelimit;
-
- public String filetypeall;
-
- public String plan_type;
-
- public String pushNotification;
-
- public String weatherQty;
-
- public String weatherFreeQty;
-
- public String reportingDays;
-
- public String weatherHourlyUpdates;
-
- public String freeEnthusiastPlans;
-
- public String visible;
-
- public String contractorPurchasable;
-
- public String boc;
-
- public String expiry;
-
- public String start;
-
- public String customerplanId;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-import com.google.gson.annotations.SerializedName;
-
-/**
- * The {@link Relay} class models the Relay response message
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class Relay {
-
- public Integer relayId;
-
- public Integer relay;
-
- public String name;
-
- public String icon;
-
- public String lastwater;
-
- public Integer time;
-
- public Integer type;
-
- @SerializedName("run")
- public String runTime;
-
- @SerializedName("run_seconds")
- public Integer runTimeSeconds;
-
- public String nicetime;
-
- public String id;
-
- /**
- * Returns back the actual relay number when multiple controllers are chained.
- *
- * @return
- */
- public int getRelayNumber() {
- int quotient = relay / 100;
- return (relay - (quotient * 100)) + (quotient * 12);
- }
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-/**
- * The {@link Response} class models Response messages
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class Response {
-
- public String errorMsg;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-/**
- * The {@link Running} class models a running relay
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class Running {
-
- public String relay;
-
- public String relayId;
-
- public Integer timeLeft;
-
- public String run;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-import java.util.List;
-
-/**
- * The {@link Sensor} class models a sensor
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class Sensor {
-
- public Integer input;
-
- public Integer type;
-
- public Integer mode;
-
- public Integer timer;
-
- public Integer offtimer;
-
- public String name;
-
- public Integer offlevel;
-
- public Integer active;
-
- public List<Object> relays = null;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-/**
- * The {@link SetControllerResponse} class models the SetController response message
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class SetControllerResponse extends Response {
-
- public String name;
-
- public String controllerId;
-
- public String message;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-/**
- * The {@link SetZoneResponse} class models the SetZone response message
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class SetZoneResponse extends Response {
-
- public String message;
-
- public String messageType;
-}
+++ /dev/null
-/**
- * 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.hydrawise.internal.api.model;
-
-import java.util.LinkedList;
-import java.util.List;
-
-/**
- * The {@link StatusScheduleResponse} class models the Status and Schedule response message
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class StatusScheduleResponse extends LocalScheduleResponse {
-
- public Integer controllerId;
-
- public Integer customerId;
-
- public Integer userId;
-
- public Integer nextpoll;
-
- public List<Sensor> sensors = new LinkedList<>();
-
- public String message;
-
- public String obsRain;
-
- public String obsRainWeek;
-
- public String obsMaxtemp;
-
- public Integer obsRainUpgrade;
-
- public String obsRainText;
-
- public String obsCurrenttemp;
-
- public String wateringTime;
-
- public Integer waterSaving;
-
- public String lastContact;
-
- public List<Forecast> forecast = new LinkedList<>();
-
- public String status;
-
- public String statusIcon;
-}
--- /dev/null
+/**
+ * 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.hydrawise.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link HydrawiseAccountConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class HydrawiseAccountConfiguration {
+ public String userName = "";
+ public String password = "";
+ public Boolean savePassword = false;
+ public Integer refreshInterval = 60;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link HydrawiseControllerConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class HydrawiseControllerConfiguration {
+ /**
+ * optional id of the controller to connect to
+ */
+ public Integer controllerId = -1;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link HydrawiseLocalConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class HydrawiseLocalConfiguration {
+ /**
+ * Host or IP for local controller
+ */
+ public String host = "";
+ /**
+ * User name (admin) for local controller
+ */
+ public String username = "";
+ /**
+ * Password for local controller
+ */
+ public String password = "";
+ /**
+ * refresh interval in seconds.
+ */
+ public int refresh = 30;
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.discovery;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants;
+import org.openhab.binding.hydrawise.internal.HydrawiseControllerListener;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.Controller;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.Customer;
+import org.openhab.binding.hydrawise.internal.handler.HydrawiseAccountHandler;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ *
+ * @author Dan Cunningham - Initial contribution
+ *
+ */
+
+@NonNullByDefault
+@Component(service = ThingHandlerService.class)
+public class HydrawiseCloudControllerDiscoveryService extends AbstractDiscoveryService
+ implements HydrawiseControllerListener, ThingHandlerService {
+
+ private static final int TIMEOUT = 5;
+ @Nullable
+ HydrawiseAccountHandler handler;
+
+ public HydrawiseCloudControllerDiscoveryService() {
+ super(Collections.singleton(HydrawiseBindingConstants.THING_TYPE_CONTROLLER), TIMEOUT, true);
+ }
+
+ @Override
+ protected void startScan() {
+ HydrawiseAccountHandler localHandler = this.handler;
+ if (localHandler != null) {
+ Customer data = localHandler.lastData();
+ if (data != null) {
+ data.controllers.forEach(controller -> addDiscoveryResults(controller));
+ }
+ }
+ }
+
+ @Override
+ public void deactivate() {
+ HydrawiseAccountHandler localHandler = this.handler;
+ if (localHandler != null) {
+ removeOlderResults(new Date().getTime(), localHandler.getThing().getUID());
+ }
+ }
+
+ @Override
+ protected synchronized void stopScan() {
+ super.stopScan();
+ HydrawiseAccountHandler localHandler = this.handler;
+ if (localHandler != null) {
+ removeOlderResults(getTimestampOfLastScan(), localHandler.getThing().getUID());
+ }
+ }
+
+ @Override
+ public void onData(List<Controller> controllers) {
+ controllers.forEach(controller -> addDiscoveryResults(controller));
+ }
+
+ @Override
+ public void setThingHandler(ThingHandler handler) {
+ this.handler = (HydrawiseAccountHandler) handler;
+ this.handler.addControllerListeners(this);
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return handler;
+ }
+
+ private void addDiscoveryResults(Controller controller) {
+ HydrawiseAccountHandler localHandler = this.handler;
+ if (localHandler != null) {
+ String label = String.format("Hydrawise Controller %s", controller.name);
+ int id = controller.id;
+ ThingUID bridgeUID = localHandler.getThing().getUID();
+ ThingUID thingUID = new ThingUID(HydrawiseBindingConstants.THING_TYPE_CONTROLLER, bridgeUID,
+ String.valueOf(id));
+ thingDiscovered(DiscoveryResultBuilder.create(thingUID).withLabel(label).withBridge(bridgeUID)
+ .withProperty(HydrawiseBindingConstants.CONFIG_CONTROLLER_ID, id)
+ .withRepresentationProperty(String.valueOf(HydrawiseBindingConstants.CONFIG_CONTROLLER_ID))
+ .build());
+ }
+ }
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.handler;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.hydrawise.internal.HydrawiseControllerListener;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
+import org.openhab.binding.hydrawise.internal.api.graphql.HydrawiseGraphQLClient;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.Customer;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.QueryResponse;
+import org.openhab.binding.hydrawise.internal.config.HydrawiseAccountConfiguration;
+import org.openhab.binding.hydrawise.internal.discovery.HydrawiseCloudControllerDiscoveryService;
+import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthException;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
+import org.openhab.core.config.core.Configuration;
+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.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link HydrawiseAccountHandler} is responsible for handling for connecting to a Hydrawise account and polling for
+ * controller data
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class HydrawiseAccountHandler extends BaseBridgeHandler implements AccessTokenRefreshListener {
+ private final Logger logger = LoggerFactory.getLogger(HydrawiseAccountHandler.class);
+ /**
+ * Minimum amount of time we can poll for updates
+ */
+ private static final int MIN_REFRESH_SECONDS = 30;
+ private static final String BASE_URL = "https://app.hydrawise.com/api/v2/";
+ private static final String AUTH_URL = BASE_URL + "oauth/access-token";
+ private static final String CLIENT_SECRET = "zn3CrjglwNV1";
+ private static final String CLIENT_ID = "hydrawise_app";
+ private static final String SCOPE = "all";
+ private final List<HydrawiseControllerListener> controllerListeners = new ArrayList<HydrawiseControllerListener>();
+ private final HydrawiseGraphQLClient apiClient;
+ private final OAuthClientService oAuthService;
+ private @Nullable ScheduledFuture<?> pollFuture;
+ private @Nullable Customer lastData;
+ private int refresh;
+
+ public HydrawiseAccountHandler(final Bridge bridge, final HttpClient httpClient, final OAuthFactory oAuthFactory) {
+ super(bridge);
+ this.oAuthService = oAuthFactory.createOAuthClientService(getThing().toString(), AUTH_URL, AUTH_URL, CLIENT_ID,
+ CLIENT_SECRET, SCOPE, false);
+ oAuthService.addAccessTokenRefreshListener(this);
+ this.apiClient = new HydrawiseGraphQLClient(httpClient, oAuthService);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ }
+
+ @Override
+ public void initialize() {
+ logger.debug("Handler initialized.");
+ scheduler.schedule(this::configure, 0, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void dispose() {
+ logger.debug("Handler disposed.");
+ clearPolling();
+ }
+
+ @Override
+ public void onAccessTokenResponse(AccessTokenResponse tokenResponse) {
+ logger.debug("Auth Token Refreshed, expires in {}", tokenResponse.getExpiresIn());
+ }
+
+ @Override
+ public Collection<Class<? extends ThingHandlerService>> getServices() {
+ return Collections.singleton(HydrawiseCloudControllerDiscoveryService.class);
+ }
+
+ public void addControllerListeners(HydrawiseControllerListener listener) {
+ this.controllerListeners.add(listener);
+ Customer data = lastData;
+ if (data != null) {
+ listener.onData(data.controllers);
+ }
+ }
+
+ public void removeControllerListeners(HydrawiseControllerListener listener) {
+ this.controllerListeners.remove(listener);
+ }
+
+ public @Nullable HydrawiseGraphQLClient graphQLClient() {
+ return apiClient;
+ }
+
+ public @Nullable Customer lastData() {
+ return lastData;
+ }
+
+ public void refreshData(int delaySeconds) {
+ initPolling(delaySeconds, this.refresh);
+ }
+
+ private void configure() {
+ HydrawiseAccountConfiguration config = getConfig().as(HydrawiseAccountConfiguration.class);
+ try {
+ if (!config.userName.isEmpty() && !config.password.isEmpty()) {
+ if (!config.savePassword) {
+ Configuration editedConfig = editConfiguration();
+ editedConfig.remove("password");
+ updateConfiguration(editedConfig);
+ }
+ oAuthService.getAccessTokenByResourceOwnerPasswordCredentials(config.userName, config.password, SCOPE);
+ } else if (oAuthService.getAccessTokenResponse() == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Login credentials required.");
+ return;
+ }
+ this.refresh = Math.max(config.refreshInterval, MIN_REFRESH_SECONDS);
+ initPolling(0, refresh);
+ } catch (OAuthException | IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ } catch (OAuthResponseException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Login credentials required.");
+ }
+ }
+
+ /**
+ * Starts/Restarts polling with an initial delay. This allows changes in the poll cycle for when commands are sent
+ * and we need to poll sooner then the next refresh cycle.
+ */
+ private synchronized void initPolling(int initalDelay, int refresh) {
+ clearPolling();
+ pollFuture = scheduler.scheduleWithFixedDelay(this::poll, initalDelay, refresh, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Stops/clears this thing's polling future
+ */
+ private void clearPolling() {
+ ScheduledFuture<?> localFuture = pollFuture;
+ if (isFutureValid(localFuture)) {
+ if (localFuture != null) {
+ localFuture.cancel(false);
+ }
+ }
+ }
+
+ private boolean isFutureValid(@Nullable ScheduledFuture<?> future) {
+ return future != null && !future.isCancelled();
+ }
+
+ private void poll() {
+ poll(true);
+ }
+
+ private void poll(boolean retry) {
+ try {
+ QueryResponse response = apiClient.queryControllers();
+ if (response == null) {
+ throw new HydrawiseConnectionException("Malformed response");
+ }
+ if (response.errors != null && response.errors.size() > 0) {
+ throw new HydrawiseConnectionException(response.errors.stream().map(error -> error.message).reduce("",
+ (messages, message) -> messages + message + ". "));
+ }
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ lastData = response.data.me;
+ controllerListeners.forEach(listener -> {
+ listener.onData(response.data.me.controllers);
+ });
+ } catch (HydrawiseConnectionException e) {
+ if (retry) {
+ logger.debug("Retrying failed poll", e);
+ poll(false);
+ } else {
+ logger.debug("Will try again during next poll period", e);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+ } catch (HydrawiseAuthenticationException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+ clearPolling();
+ }
+ }
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.handler;
+
+import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
+
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.measure.quantity.Speed;
+import javax.measure.quantity.Temperature;
+import javax.measure.quantity.Volume;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.hydrawise.internal.HydrawiseControllerListener;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
+import org.openhab.binding.hydrawise.internal.api.graphql.HydrawiseGraphQLClient;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.Controller;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.Forecast;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.Sensor;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.UnitValue;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.Zone;
+import org.openhab.binding.hydrawise.internal.api.graphql.dto.ZoneRun;
+import org.openhab.binding.hydrawise.internal.config.HydrawiseControllerConfiguration;
+import org.openhab.core.library.types.DateTimeType;
+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.ImperialUnits;
+import org.openhab.core.library.unit.SIUnits;
+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.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link HydrawiseControllerHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+
+@NonNullByDefault
+public class HydrawiseControllerHandler extends BaseThingHandler implements HydrawiseControllerListener {
+ private final Logger logger = LoggerFactory.getLogger(HydrawiseControllerHandler.class);
+ private static final int DEFAULT_SUSPEND_TIME_HOURS = 24;
+ private static final int DEFAULT_REFRESH_SECONDS = 15;
+ // All responses use US local time formats
+ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM uu HH:mm:ss Z",
+ Locale.US);
+ private final Map<String, @Nullable State> stateMap = Collections
+ .synchronizedMap(new HashMap<String, @Nullable State>());
+ private final Map<String, @Nullable Zone> zoneMaps = Collections
+ .synchronizedMap(new HashMap<String, @Nullable Zone>());
+ private int controllerId;
+
+ public HydrawiseControllerHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void initialize() {
+ HydrawiseControllerConfiguration config = getConfigAs(HydrawiseControllerConfiguration.class);
+ controllerId = config.controllerId;
+ Bridge bridge = getBridge();
+ if (bridge != null) {
+ HydrawiseAccountHandler handler = (HydrawiseAccountHandler) bridge.getHandler();
+ if (handler != null) {
+ handler.addControllerListeners(this);
+ if (bridge.getStatus() == ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("handleCommand channel {} Command {}", channelUID.getAsString(), command.toFullString());
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ logger.debug("Controller is NOT ONLINE and is not responding to commands");
+ return;
+ }
+
+ // remove our cached state for this, will be safely updated on next poll
+ stateMap.remove(channelUID.getAsString());
+
+ if (command instanceof RefreshType) {
+ // we already removed this from the cache
+ return;
+ }
+
+ HydrawiseGraphQLClient client = apiClient();
+ if (client == null) {
+ logger.debug("API client not found");
+ return;
+ }
+
+ String group = channelUID.getGroupId();
+ String channelId = channelUID.getIdWithoutGroup();
+ boolean allCommand = CHANNEL_GROUP_ALLZONES.equals(group);
+ Zone zone = zoneMaps.get(group);
+
+ if (!allCommand && zone == null) {
+ logger.debug("Zone not found {}", group);
+ return;
+ }
+
+ try {
+ switch (channelId) {
+ case CHANNEL_ZONE_RUN_CUSTOM:
+ if (!(command instanceof QuantityType<?>)) {
+ logger.warn("Invalid command type for run custom {}", command.getClass().getName());
+ return;
+ }
+ QuantityType<?> time = ((QuantityType<?>) command).toUnit(Units.SECOND);
+
+ if (time == null) {
+ return;
+ }
+
+ if (allCommand) {
+ client.runAllRelays(controllerId, time.intValue());
+ } else if (zone != null) {
+ client.runRelay(zone.id, time.intValue());
+ }
+ break;
+ case CHANNEL_ZONE_RUN:
+ if (!(command instanceof OnOffType)) {
+ logger.warn("Invalid command type for run {}", command.getClass().getName());
+ return;
+ }
+ if (allCommand) {
+ if (command == OnOffType.ON) {
+ client.runAllRelays(controllerId);
+ } else {
+ client.stopAllRelays(controllerId);
+ }
+ } else if (zone != null) {
+ if (command == OnOffType.ON) {
+ client.runRelay(zone.id);
+ } else {
+ client.stopRelay(zone.id);
+ }
+ }
+ break;
+ case CHANNEL_ZONE_SUSPEND:
+ if (!(command instanceof OnOffType)) {
+ logger.warn("Invalid command type for suspend {}", command.getClass().getName());
+ return;
+ }
+ if (allCommand) {
+ if (command == OnOffType.ON) {
+ client.suspendAllRelays(controllerId, OffsetDateTime.now(ZoneOffset.UTC)
+ .plus(DEFAULT_SUSPEND_TIME_HOURS, ChronoUnit.HOURS).format(DATE_FORMATTER));
+ } else {
+ client.resumeAllRelays(controllerId);
+ }
+ } else if (zone != null) {
+ if (command == OnOffType.ON) {
+ client.suspendRelay(zone.id, OffsetDateTime.now(ZoneOffset.UTC)
+ .plus(DEFAULT_SUSPEND_TIME_HOURS, ChronoUnit.HOURS).format(DATE_FORMATTER));
+ } else {
+ client.resumeRelay(zone.id);
+ }
+ }
+ break;
+ case CHANNEL_ZONE_SUSPENDUNTIL:
+ if (!(command instanceof DateTimeType)) {
+ logger.warn("Invalid command type for suspend {}", command.getClass().getName());
+ return;
+ }
+ if (allCommand) {
+ client.suspendAllRelays(controllerId,
+ ((DateTimeType) command).getZonedDateTime().format(DATE_FORMATTER));
+ } else if (zone != null) {
+ client.suspendRelay(zone.id,
+ ((DateTimeType) command).getZonedDateTime().format(DATE_FORMATTER));
+ }
+ break;
+ default:
+ logger.warn("Uknown channelId {}", channelId);
+ return;
+ }
+ HydrawiseAccountHandler handler = getAccountHandler();
+ if (handler != null) {
+ handler.refreshData(DEFAULT_REFRESH_SECONDS);
+ }
+ } catch (HydrawiseCommandException | HydrawiseConnectionException e) {
+ logger.debug("Could not issue command", e);
+ } catch (HydrawiseAuthenticationException e) {
+ logger.debug("Credentials not valid");
+ }
+ }
+
+ @Override
+ public void onData(List<Controller> controllers) {
+ logger.trace("onData my controller id {}", controllerId);
+ controllers.stream().filter(c -> c.id == controllerId).findFirst().ifPresent(controller -> {
+ logger.trace("Updating Controller {} sensors {} forecast {} ", controller.id, controller.sensors,
+ controller.location.forecast);
+ updateController(controller);
+ if (controller.sensors != null) {
+ updateSensors(controller.sensors);
+ }
+ if (controller.location != null && controller.location.forecast != null) {
+ updateForecast(controller.location.forecast);
+ }
+ if (controller.zones != null) {
+ updateZones(controller.zones);
+ }
+
+ // update values with what the cloud tells us even though the controller may be offline
+ if (!controller.status.online) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Controller Offline: %s last seen %s", controller.status.summary,
+ secondsToDateTime(controller.status.lastContact.timestamp)));
+ } else if (getThing().getStatus() != ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ });
+ }
+
+ @Override
+ public void channelLinked(ChannelUID channelUID) {
+ // clear our cached value so the new channel gets updated on the next poll
+ stateMap.remove(channelUID.getId());
+ }
+
+ private void updateController(Controller controller) {
+ updateGroupState(CHANNEL_GROUP_CONTROLLER_SYSTEM, CHANNEL_CONTROLLER_NAME, new StringType(controller.name));
+ updateGroupState(CHANNEL_GROUP_CONTROLLER_SYSTEM, CHANNEL_CONTROLLER_SUMMARY,
+ new StringType(controller.status.summary));
+ updateGroupState(CHANNEL_GROUP_CONTROLLER_SYSTEM, CHANNEL_CONTROLLER_LAST_CONTACT,
+ secondsToDateTime(controller.status.lastContact.timestamp));
+ }
+
+ private void updateZones(List<Zone> zones) {
+ AtomicReference<Boolean> anyRunning = new AtomicReference<Boolean>(false);
+ AtomicReference<Boolean> anySuspended = new AtomicReference<Boolean>(false);
+ int i = 1;
+ for (Zone zone : zones) {
+ String group = "zone" + (i++);
+ zoneMaps.put(group, zone);
+ logger.trace("Updateing Zone {} {} ", group, zone.name);
+ updateGroupState(group, CHANNEL_ZONE_NAME, new StringType(zone.name));
+ updateGroupState(group, CHANNEL_ZONE_ICON, new StringType(BASE_IMAGE_URL + zone.icon.fileName));
+ if (zone.scheduledRuns != null) {
+ updateGroupState(group, CHANNEL_ZONE_SUMMARY,
+ zone.scheduledRuns.summary != null ? new StringType(zone.scheduledRuns.summary)
+ : UnDefType.UNDEF);
+ ZoneRun nextRun = zone.scheduledRuns.nextRun;
+ if (nextRun != null) {
+ updateGroupState(group, CHANNEL_ZONE_DURATION, new QuantityType<>(nextRun.duration, Units.MINUTE));
+ updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME,
+ secondsToDateTime(nextRun.startTime.timestamp));
+ } else {
+ updateGroupState(group, CHANNEL_ZONE_DURATION, UnDefType.UNDEF);
+ updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME, UnDefType.UNDEF);
+ }
+ ZoneRun currRunn = zone.scheduledRuns.currentRun;
+ if (currRunn != null) {
+ updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.ON);
+ updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new QuantityType<>(
+ currRunn.endTime.timestamp - Instant.now().getEpochSecond(), Units.SECOND));
+ anyRunning.set(true);
+ } else {
+ updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.OFF);
+ updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new QuantityType<>(0, Units.MINUTE));
+ }
+ }
+ if (zone.status.suspendedUntil != null) {
+ updateGroupState(group, CHANNEL_ZONE_SUSPEND, OnOffType.ON);
+ updateGroupState(group, CHANNEL_ZONE_SUSPENDUNTIL,
+ secondsToDateTime(zone.status.suspendedUntil.timestamp));
+ anySuspended.set(true);
+ } else {
+ updateGroupState(group, CHANNEL_ZONE_SUSPEND, OnOffType.OFF);
+ updateGroupState(group, CHANNEL_ZONE_SUSPENDUNTIL, UnDefType.UNDEF);
+ }
+ }
+ updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_RUN, anyRunning.get() ? OnOffType.ON : OnOffType.OFF);
+ updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_SUSPEND,
+ anySuspended.get() ? OnOffType.ON : OnOffType.OFF);
+ updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_SUSPENDUNTIL, UnDefType.UNDEF);
+ }
+
+ private void updateSensors(List<Sensor> sensors) {
+ int i = 1;
+ for (Sensor sensor : sensors) {
+ String group = "sensor" + (i++);
+ updateGroupState(group, CHANNEL_SENSOR_NAME, new StringType(sensor.name));
+ if (sensor.model.offTimer != null) {
+ updateGroupState(group, CHANNEL_SENSOR_OFFTIMER,
+ new QuantityType<>(sensor.model.offTimer, Units.SECOND));
+ }
+ if (sensor.model.delay != null) {
+ updateGroupState(group, CHANNEL_SENSOR_DELAY, new QuantityType<>(sensor.model.delay, Units.SECOND));
+ }
+ if (sensor.model.offLevel != null) {
+ updateGroupState(group, CHANNEL_SENSOR_OFFLEVEL, new DecimalType(sensor.model.offLevel));
+ }
+ if (sensor.status.active != null) {
+ updateGroupState(group, CHANNEL_SENSOR_ACTIVE, sensor.status.active ? OnOffType.ON : OnOffType.OFF);
+ }
+ if (sensor.status.waterFlow != null) {
+ updateGroupState(group, CHANNEL_SENSOR_WATERFLOW,
+ waterFlowToQuantityType(sensor.status.waterFlow.value, sensor.status.waterFlow.unit));
+ }
+ }
+ }
+
+ private void updateForecast(List<Forecast> forecasts) {
+ int i = 1;
+ for (Forecast forecast : forecasts) {
+ String group = "forecast" + (i++);
+ updateGroupState(group, CHANNEL_FORECAST_TIME, stringToDateTime(forecast.time));
+ updateGroupState(group, CHANNEL_FORECAST_CONDITIONS, new StringType(forecast.conditions));
+ updateGroupState(group, CHANNEL_FORECAST_HUMIDITY, new DecimalType(forecast.averageHumidity.intValue()));
+ updateTemperature(forecast.highTemperature, group, CHANNEL_FORECAST_TEMPERATURE_HIGH);
+ updateTemperature(forecast.lowTemperature, group, CHANNEL_FORECAST_TEMPERATURE_LOW);
+ updateWindspeed(forecast.averageWindSpeed, group, CHANNEL_FORECAST_WIND);
+ // this seems to sometimes be optional
+ if (forecast.evapotranspiration != null) {
+ updateGroupState(group, CHANNEL_FORECAST_EVAPOTRANSPRIATION,
+ new DecimalType(forecast.evapotranspiration.value.floatValue()));
+ }
+ updateGroupState(group, CHANNEL_FORECAST_PRECIPITATION,
+ new DecimalType(forecast.precipitation.value.floatValue()));
+ updateGroupState(group, CHANNEL_FORECAST_PROBABILITYOFPRECIPITATION,
+ new DecimalType(forecast.probabilityOfPrecipitation));
+
+ }
+ }
+
+ private void updateTemperature(UnitValue temperature, String group, String channel) {
+ logger.debug("TEMP {} {} {} {}", group, channel, temperature.unit, temperature.value);
+ updateGroupState(group, channel, new QuantityType<Temperature>(temperature.value,
+ "\\u00b0F".equals(temperature.unit) ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS));
+ }
+
+ private void updateWindspeed(UnitValue wind, String group, String channel) {
+ updateGroupState(group, channel, new QuantityType<Speed>(wind.value,
+ "mph".equals(wind.unit) ? ImperialUnits.MILES_PER_HOUR : SIUnits.KILOMETRE_PER_HOUR));
+ }
+
+ private void updateGroupState(String group, String channelID, State state) {
+ String channelName = group + "#" + channelID;
+ State oldState = stateMap.put(channelName, state);
+ if (!state.equals(oldState)) {
+ ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelName);
+ logger.debug("updateState updating {} {}", channelUID, state);
+ updateState(channelUID, state);
+ }
+ }
+
+ @Nullable
+ private HydrawiseAccountHandler getAccountHandler() {
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ logger.warn("No bridge found for thing");
+ return null;
+ }
+ BridgeHandler handler = bridge.getHandler();
+ if (handler == null) {
+ logger.warn("No handler found for bridge");
+ return null;
+ }
+ return ((HydrawiseAccountHandler) handler);
+ }
+
+ @Nullable
+ private HydrawiseGraphQLClient apiClient() {
+ HydrawiseAccountHandler handler = getAccountHandler();
+ if (handler == null) {
+ return null;
+ } else {
+ return handler.graphQLClient();
+ }
+ }
+
+ private DateTimeType secondsToDateTime(Integer seconds) {
+ Instant instant = Instant.ofEpochSecond(seconds);
+ ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneOffset.UTC);
+ return new DateTimeType(zdt);
+ }
+
+ private DateTimeType stringToDateTime(String date) {
+ ZonedDateTime zdt = ZonedDateTime.parse(date, DATE_FORMATTER);
+ return new DateTimeType(zdt);
+ }
+
+ private QuantityType<Volume> waterFlowToQuantityType(Number flow, String units) {
+ double waterFlow = flow.doubleValue();
+ if ("gals".equals(units)) {
+ waterFlow = waterFlow * 3.785;
+ }
+ return new QuantityType<>(waterFlow, Units.LITRE);
+ }
+}
--- /dev/null
+/**
+ * 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.hydrawise.internal.handler;
+
+import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
+
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
+import org.openhab.binding.hydrawise.internal.api.local.HydrawiseLocalApiClient;
+import org.openhab.binding.hydrawise.internal.api.local.dto.LocalScheduleResponse;
+import org.openhab.binding.hydrawise.internal.api.local.dto.Relay;
+import org.openhab.binding.hydrawise.internal.api.local.dto.Running;
+import org.openhab.binding.hydrawise.internal.config.HydrawiseLocalConfiguration;
+import org.openhab.core.library.types.DateTimeType;
+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.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link HydrawiseLocalHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class HydrawiseLocalHandler extends BaseThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(HydrawiseLocalHandler.class);
+ protected final Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<>());
+ protected final Map<String, Relay> relayMap = Collections.synchronizedMap(new HashMap<>());
+ private @Nullable ScheduledFuture<?> pollFuture;
+
+ /**
+ * value observed being used by the Hydrawise clients as a max time value,
+ */
+ private static final long MAX_RUN_TIME = 157680000;
+
+ /**
+ * Minimum amount of time we can poll for updates
+ */
+ protected static final int MIN_REFRESH_SECONDS = 5;
+
+ /**
+ * Minimum amount of time we can poll after a command
+ */
+ protected static final int COMMAND_REFRESH_SECONDS = 5;
+
+ /**
+ * Our poll rate
+ */
+ protected int refresh;
+
+ /**
+ * Future to poll for updated
+ */
+
+ HydrawiseLocalApiClient client;
+
+ public HydrawiseLocalHandler(Thing thing, HttpClient httpClient) {
+ super(thing);
+ client = new HydrawiseLocalApiClient(httpClient);
+ }
+
+ @Override
+ public void initialize() {
+ scheduler.schedule(this::configureInternal, 0, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void dispose() {
+ logger.debug("Handler disposed.");
+ clearPolling();
+ }
+
+ @Override
+ public void channelLinked(ChannelUID channelUID) {
+ // clear our cached value so the new channel gets updated on the next poll
+ stateMap.remove(channelUID.getId());
+ }
+
+ @SuppressWarnings({ "null", "unused" }) // compiler does not like relayMap.get can return null
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ logger.warn("Controller is NOT ONLINE and is not responding to commands");
+ return;
+ }
+
+ // remove our cached state for this, will be safely updated on next poll
+ stateMap.remove(channelUID.getAsString());
+
+ if (command instanceof RefreshType) {
+ // we already removed this from the cache
+ return;
+ }
+
+ String group = channelUID.getGroupId();
+ String channelId = channelUID.getIdWithoutGroup();
+ boolean allCommand = CHANNEL_GROUP_ALLZONES.equals(group);
+
+ Relay relay = relayMap.get(group);
+ if (!allCommand && relay == null) {
+ logger.debug("Zone not found {}", group);
+ return;
+ }
+
+ try {
+ clearPolling();
+ switch (channelId) {
+ case CHANNEL_ZONE_RUN_CUSTOM:
+ if (!(command instanceof QuantityType<?>)) {
+ logger.warn("Invalid command type for run custom {}", command.getClass().getName());
+ return;
+ }
+ if (allCommand) {
+ client.runAllRelays(((QuantityType<?>) command).intValue());
+ } else {
+ client.runRelay(((QuantityType<?>) command).intValue(), relay.relay);
+ }
+ break;
+ case CHANNEL_ZONE_RUN:
+ if (!(command instanceof OnOffType)) {
+ logger.warn("Invalid command type for run {}", command.getClass().getName());
+ return;
+ }
+ if (allCommand) {
+ if (command == OnOffType.ON) {
+ client.runAllRelays();
+ } else {
+ client.stopAllRelays();
+ }
+ } else {
+ if (command == OnOffType.ON) {
+ client.runRelay(relay.relay);
+ } else {
+ client.stopRelay(relay.relay);
+ }
+ }
+ break;
+ }
+ initPolling(COMMAND_REFRESH_SECONDS);
+ } catch (HydrawiseCommandException | HydrawiseConnectionException e) {
+ logger.debug("Could not issue command", e);
+ initPolling(COMMAND_REFRESH_SECONDS);
+ } catch (HydrawiseAuthenticationException e) {
+ logger.debug("Credentials not valid");
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
+ configureInternal();
+ }
+ }
+
+ protected void updateZones(LocalScheduleResponse status) {
+ ZonedDateTime now = ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ status.relays.forEach(r -> {
+ String group = "zone" + r.getRelayNumber();
+ relayMap.put(group, r);
+ logger.trace("Updateing Zone {} {} ", group, r.name);
+ updateGroupState(group, CHANNEL_ZONE_NAME, new StringType(r.name));
+ updateGroupState(group, CHANNEL_ZONE_TYPE, new DecimalType(r.type));
+ updateGroupState(group, CHANNEL_ZONE_STARTTIME,
+ r.runSeconds != null ? new QuantityType<>(r.runSeconds, Units.SECOND) : UnDefType.UNDEF);
+ if (r.time >= MAX_RUN_TIME) {
+ updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME, UnDefType.UNDEF);
+ } else {
+ updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME,
+ new DateTimeType(now.plusSeconds(r.time).truncatedTo(ChronoUnit.MINUTES)));
+ }
+
+ Optional<Running> running = status.running.stream()
+ .filter(z -> Integer.parseInt(z.relayId) == r.relayId.intValue()).findAny();
+ if (running.isPresent()) {
+ updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.ON);
+ updateGroupState(group, CHANNEL_ZONE_TIME_LEFT,
+ new QuantityType<>(running.get().timeLeft, Units.SECOND));
+ logger.debug("{} Time Left {}", r.name, running.get().timeLeft);
+
+ } else {
+ updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.OFF);
+ updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new QuantityType<>(0, Units.SECOND));
+ }
+
+ updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_RUN,
+ status.running.size() > 0 ? OnOffType.ON : OnOffType.OFF);
+ });
+ }
+
+ @SuppressWarnings("serial")
+ protected class NotConfiguredException extends Exception {
+ NotConfiguredException(String message) {
+ super(message);
+ }
+ }
+
+ private boolean isFutureValid(@Nullable ScheduledFuture<?> future) {
+ return future != null && !future.isCancelled();
+ }
+
+ private void configureInternal() {
+ clearPolling();
+ stateMap.clear();
+ relayMap.clear();
+ try {
+ HydrawiseLocalConfiguration configuration = getConfig().as(HydrawiseLocalConfiguration.class);
+ this.refresh = Math.max(configuration.refresh, MIN_REFRESH_SECONDS);
+ logger.trace("Connecting to host {}", configuration.host);
+ client.setCredentials(configuration.host, configuration.username, configuration.password);
+ LocalScheduleResponse response = client.getLocalSchedule();
+ if (response != null) {
+ updateZones(response);
+ initPolling(refresh);
+ } else {
+ logger.debug("Could not connect to service");
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Invalid response from service");
+ }
+ } catch (HydrawiseConnectionException e) {
+ logger.debug("Could not connect to service");
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ } catch (HydrawiseAuthenticationException e) {
+ logger.debug("Credentials not valid");
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
+ }
+ }
+
+ /**
+ * Starts/Restarts polling with an initial delay. This allows changes in the poll cycle for when commands are sent
+ * and we need to poll sooner then the next refresh cycle.
+ */
+ private synchronized void initPolling(int initalDelay) {
+ clearPolling();
+ pollFuture = scheduler.scheduleWithFixedDelay(this::pollControllerInternal, initalDelay, refresh,
+ TimeUnit.SECONDS);
+ }
+
+ /**
+ * Stops/clears this thing's polling future
+ */
+ private void clearPolling() {
+ ScheduledFuture<?> localFuture = pollFuture;
+ if (isFutureValid(localFuture)) {
+ if (localFuture != null) {
+ localFuture.cancel(false);
+ }
+ }
+ }
+
+ /**
+ * Poll the controller for updates.
+ */
+ private void pollControllerInternal() {
+ try {
+ LocalScheduleResponse response = client.getLocalSchedule();
+ if (response != null) {
+ updateZones(response);
+ }
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ } catch (HydrawiseConnectionException e) {
+ // poller will continue to run, set offline until next run
+ logger.debug("Exception polling", e);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ } catch (HydrawiseAuthenticationException e) {
+ // if are creds are not valid, we need to try re authorizing again
+ logger.debug("Authorization exception during polling", e);
+ configureInternal();
+ }
+ }
+
+ private void updateGroupState(String group, String channelID, State state) {
+ String channelName = group + "#" + channelID;
+ State oldState = stateMap.put(channelName, state);
+ if (!state.equals(oldState)) {
+ ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelName);
+ logger.debug("updateState updating {} {}", channelUID, state);
+ updateState(channelUID, state);
+ }
+ }
+}
<channels>
<channel id="name" typeId="name"/>
<channel id="icon" typeId="icon"/>
- <channel id="time" typeId="time"/>
<channel id="type" typeId="type"/>
- <channel id="runcustom" typeId="runcustom"/>
<channel id="run" typeId="run"/>
+ <channel id="runcustom" typeId="runcustom"/>
<channel id="nextruntime" typeId="nextruntime"/>
+ <channel id="suspend" typeId="suspend"/>
+ <channel id="suspenduntil" typeId="suspenduntil"/>
<channel id="timeleft" typeId="timeleft"/>
+ <channel id="summary" typeId="summary"/>
</channels>
</channel-group-type>
<channels>
<channel id="runcustom" typeId="runcustom"/>
<channel id="run" typeId="run"/>
+ <channel id="suspend" typeId="suspend"/>
+ <channel id="suspenduntil" typeId="suspenduntil"/>
</channels>
</channel-group-type>
<channels>
<channel id="name" typeId="name"/>
<channel id="input" typeId="input"/>
- <channel id="mode" typeId="mode"/>
- <channel id="timer" typeId="timer"/>
+ <channel id="delay" typeId="delay"/>
<channel id="offtimer" typeId="offtimer"/>
<channel id="offlevel" typeId="offlevel"/>
<channel id="active" typeId="active"/>
+ <channel id="waterflow" typeId="waterflow"/>
</channels>
</channel-group-type>
<channel id="temperaturehigh" typeId="temperaturehigh"/>
<channel id="temperaturelow" typeId="temperaturelow"/>
<channel id="conditions" typeId="conditions"/>
- <channel id="day" typeId="day"/>
+ <channel id="time" typeId="time"/>
<channel id="humidity" typeId="humidity"/>
<channel id="wind" typeId="wind"/>
+ <channel id="evapotranspiration" typeId="evapotranspiration"/>
+ <channel id="precipitation" typeId="precipitation"/>
+ <channel id="probabilityofprecipitation" typeId="probabilityofprecipitation"/>
+ </channels>
+ </channel-group-type>
+
+ <channel-group-type id="system">
+ <label>System</label>
+ <description>Controller system data</description>
+ <channels>
+ <channel id="name" typeId="name"/>
+ <channel id="summary" typeId="summary"/>
+ <channel id="lastcontacttime" typeId="lastcontacttime"/>
</channels>
</channel-group-type>
<!-- Controller -->
+
+ <channel-type id="lastcontacttime" advanced="true">
+ <item-type>DateTime</item-type>
+ <label>Last Contact Time</label>
+ <description>Last contact time of a controller</description>
+ <state readOnly="true"></state>
+ </channel-type>
+
+ <channel-type id="summary">
+ <item-type>String</item-type>
+ <label>Status Summary</label>
+ <description>Status summary</description>
+ <state readOnly="true"></state>
+ </channel-type>
+
+ <!-- Zones -->
+
<channel-type id="name">
<item-type>String</item-type>
<label>Name</label>
<state readOnly="true"></state>
</channel-type>
- <channel-type id="time" advanced="true">
- <item-type>Number</item-type>
+ <channel-type id="starttime" advanced="true">
+ <item-type>DateTime</item-type>
<label>Start Time</label>
- <description>Zone start time in seconds</description>
+ <description>Next zone start time</description>
+ <state readOnly="true"></state>
+ </channel-type>
+
+ <channel-type id="duration" advanced="true">
+ <item-type>Number:Time</item-type>
+ <label>Duration</label>
+ <description>Next start duration</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="nextruntime">
<item-type>DateTime</item-type>
<label>Next Run Time</label>
- <description>Next time this zone is scheduled to run</description>
+ <description>The next time this zone is scheduled to run</description>
<state readOnly="true"></state>
</channel-type>
</channel-type>
<channel-type id="runcustom">
- <item-type>Number</item-type>
- <label>Run Zones With Custom Duration </label>
- <description>Run zones now for a custom duration of time in seconds</description>
+ <item-type>Number:Time</item-type>
+ <label>Run Zones With Custom Duration</label>
+ <description>Run zones now for a custom duration</description>
+ </channel-type>
+
+ <channel-type id="suspend">
+ <item-type>Switch</item-type>
+ <label>Suspend Zones</label>
+ <description>Suspends or resumes zones</description>
+ </channel-type>
+
+ <channel-type id="suspenduntil">
+ <item-type>DateTime</item-type>
+ <label>Suspend Zones</label>
+ <description>Suspends zones until this date</description>
</channel-type>
<channel-type id="timeleft">
- <item-type>Number</item-type>
+ <item-type>Number:Time</item-type>
<label>Time Left Seconds</label>
<description>Time left that zone will run for</description>
<state readOnly="true"></state>
<state readOnly="true"></state>
</channel-type>
- <channel-type id="mode" advanced="true">
- <item-type>Number</item-type>
- <label>Mode</label>
- <description>Sensor mode</description>
- <state readOnly="true"></state>
- </channel-type>
-
- <channel-type id="timer" advanced="true">
- <item-type>Number</item-type>
- <label>Timer</label>
- <description>Sensor timer</description>
+ <channel-type id="delay" advanced="true">
+ <item-type>Number:Time</item-type>
+ <label>Delay</label>
+ <description>Sensor delay</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="offtimer" advanced="true">
- <item-type>Number</item-type>
+ <item-type>Number:Time</item-type>
<label>Off Timer</label>
<description>Sensor off timer</description>
<state readOnly="true"></state>
<description>Sensor off level</description>
<state readOnly="true"></state>
</channel-type>
+
<channel-type id="active">
<item-type>Switch</item-type>
<label>Active</label>
<state readOnly="true"></state>
</channel-type>
+ <channel-type id="waterflow" advanced="true">
+ <item-type>Number:Volume</item-type>
+ <label>Water Flow</label>
+ <description>Sensor water flow</description>
+ <state readOnly="true"></state>
+ </channel-type>
+
<!-- Weather Forecast -->
<channel-type id="temperaturehigh">
<item-type>Number:Temperature</item-type>
<state readOnly="true"></state>
</channel-type>
- <channel-type id="day">
- <item-type>String</item-type>
- <label>Day of Week</label>
- <description>Day of week for the weather forecast</description>
+ <channel-type id="time">
+ <item-type>DateTime</item-type>
+ <label>Forecast Time</label>
+ <description>Forecast date and time</description>
<state readOnly="true"></state>
</channel-type>
<category>Wind</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
+
+ <channel-type id="evapotranspiration">
+ <item-type>Number</item-type>
+ <label>Evapotranspiration</label>
+ <description>Evapotranspiration amount</description>
+ <state readOnly="true" pattern="%.1f"/>
+ </channel-type>
+
+ <channel-type id="precipitation">
+ <item-type>Number</item-type>
+ <label>Precipitation</label>
+ <description>Precipitation amount</description>
+ <state readOnly="true" pattern="%.1f"/>
+ </channel-type>
+
+ <channel-type id="probabilityofprecipitation">
+ <item-type>Number</item-type>
+ <label>Probability Of Precipitation</label>
+ <description>Probability of precipitation percentage</description>
+ <state readOnly="true" pattern="%d%%"/>
+ </channel-type>
</thing:thing-descriptions>
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
- <!-- Sample Thing Type -->
- <thing-type id="cloud">
- <label>Hydrawise Cloud Thing</label>
- <description>Hydrawise cloud connected irrigation system</description>
+ <bridge-type id="account">
+ <label>Hydrawise Account Thing</label>
+ <description>Hydrawise account</description>
+ <config-description>
+ <parameter name="userName" type="text" required="true">
+ <label>User Name</label>
+ <description>Your Hydrawise account user name</description>
+ </parameter>
+ <parameter name="password" type="text" required="false">
+ <label>Password</label>
+ <context>password</context>
+ <description>Your Hydrawise account password, for security this will not be saved after the first login attempt
+ unless the "Save Password" option is enabled</description>
+ </parameter>
+ <parameter name="savePassword" type="boolean" required="false">
+ <label>Save Password</label>
+ <description>By default, the password will not be persisted after the first login attempt unless this is enabled</description>
+ <default>false</default>
+ </parameter>
+ <parameter name="refresh" type="integer" required="false" min="30" unit="s">
+ <label>Refresh interval</label>
+ <description>Specifies the refresh interval in seconds</description>
+ <default>60</default>
+ </parameter>
+ </config-description>
+ </bridge-type>
+
+ <thing-type id="controller">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+ <label>Hydrawise Controller Thing</label>
+ <description>Hydrawise connected irrigation controller</description>
<!-- Until we have https://github.com/eclipse/smarthome/issues/1118 fixed, we need to list all possible channel groups.
Once this is fixed we can dynamically add them to the thing and not list them here. -->
<channel-groups>
+
+ <!-- System -->
+
+ <channel-group id="system" typeId="system"/>
+
+ <!-- Sensors -->
+
<channel-group id="sensor1" typeId="sensor">
<label>Sensor 1</label>
<description>Sensor 1</description>
<description>Sensor 4</description>
</channel-group>
+ <!-- Forecasts -->
+
<channel-group id="forecast1" typeId="forecast">
<label>Today's Weather</label>
<description>Today's weather forecast</description>
<label>Day 3 Weather</label>
<description>Day 3 weather forecast</description>
</channel-group>
- <channel-group id="forecast4" typeId="forecast">
- <label>Day 4 Weather</label>
- <description>Day 4 weather forecast</description>
- </channel-group>
+
+ <!-- All Zones -->
<channel-group id="allzones" typeId="allzones"/>
+ <!-- Zones -->
+
<channel-group id="zone1" typeId="zone">
<label>Zone 1</label>
<description>Sprinkler Zone 1</description>
</channel-group>
</channel-groups>
<config-description>
- <parameter name="apiKey" type="text" required="true">
- <label>API Key</label>
- <description>API Key from https://app.hydrawise.com/config/account</description>
- </parameter>
- <parameter name="refresh" type="integer" required="true">
- <label>Refresh interval</label>
- <description>Specifies the refresh interval in seconds</description>
- <default>30</default>
- </parameter>
- <parameter name="controllerId" type="integer" required="false">
- <label>Optional Controller ID interval</label>
- <description>Optional parameter to specify the Hydrawise controller ID if you have more then one associated with
- your account.
+ <parameter name="controllerId" type="integer" required="true">
+ <label>Controller ID</label>
+ <description>The ID of a cloud connected irrigation controller
</description>
</parameter>
</config-description>
<thing-type id="local">
<label>Hydrawise Local Thing</label>
- <description>Hydrawise local connected irrigation system</description>
+ <description>Hydrawise local connected irrigation controller</description>
<channel-groups>
<channel-group id="zone1" typeId="zone">
<label>Zone 1</label>
</parameter>
</config-description>
</thing-type>
-
-
</thing:thing-descriptions>
--- /dev/null
+{
+ me {
+ email
+ lastContact
+ controllers {
+ id
+ name
+ status {
+ summary
+ online
+ lastContact {
+ timestamp
+ }
+ }
+ location {
+ coordinates {
+ latitude
+ longitude
+ }
+ forecast(days: 3) {
+ time
+ updateTime
+ conditions
+ averageWindSpeed {
+ value
+ unit
+ }
+ highTemperature {
+ value
+ unit
+ }
+ lowTemperature {
+ value
+ unit
+ }
+ probabilityOfPrecipitation
+ precipitation {
+ value
+ unit
+ }
+ averageHumidity
+ }
+ }
+ zones {
+ id
+ name
+ status {
+ suspendedUntil {
+ timestamp
+ }
+ }
+ icon {
+ id
+ fileName
+ customImage {
+ id
+ url
+ }
+ }
+ number {
+ value
+ label
+ }
+ scheduledRuns {
+ summary
+ currentRun{
+ id
+ startTime {
+ timestamp
+ }
+ endTime {
+ timestamp
+ }
+ duration
+ status {
+ value
+ label
+ }
+ }
+ nextRun {
+ id
+ startTime {
+ timestamp
+ }
+ endTime {
+ timestamp
+ }
+ duration
+ }
+ }
+ }
+ sensors {
+ id
+ name
+ input {
+ number
+ label
+ }
+ status {
+ active
+ waterFlow {
+ value
+ unit
+ }
+ }
+ model {
+ modeType
+ active
+ offLevel
+ offTimer
+ delay
+ }
+ }
+ }
+ }
+}