[hydrawise] Migrate to new GraphQL based API (#10947)

* [hydrawise] Migrated to new GraphQL based API

Fixes #7261

Signed-off-by: Dan Cunningham <dan@digitaldan.com>

* Addressed PR comments.

Signed-off-by: Dan Cunningham <dan@digitaldan.com>

* Address PR review comments.

Signed-off-by: Dan Cunningham <dan@digitaldan.com>
This commit is contained in:
Dan Cunningham 2021-08-01 11:03:37 -07:00 committed by GitHub
parent f25cc8d14a
commit e465155d84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 2369 additions and 947 deletions

View File

@ -6,20 +6,32 @@ The Hydrawise binding allows monitoring and control of [Hunter Industries's](htt
## 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
@ -27,6 +39,8 @@ The Local Thing type uses an undocumented API that allows direct HTTP access to
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.
@ -41,28 +55,29 @@ Use Cases
## Thing Configuration
### Cloud Thing
### Account 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 |
| 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 |
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:
### Controller Thing
![Account](doc/settings.png)
| Configuration Name | type | required | Comments |
|--------------------|---------|----------|----------------------|
| controllerId | Integer | True | ID of the controller |
Then copy the API key shown here:
![API Key](doc/apikey.png)
### 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 |
@ -70,6 +85,12 @@ Then copy the API key shown here:
### 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
@ -94,14 +115,13 @@ Up to 4 total sensors are supported per 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
@ -114,58 +134,81 @@ A single all zone group are supported per Cloud or Local Thing
### 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"}
```

View File

@ -23,44 +23,57 @@ import org.openhab.core.thing.ThingTypeUID;
*/
@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";

View File

@ -1,243 +0,0 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}
}

View File

@ -0,0 +1,28 @@
/**
* 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);
}

View File

@ -15,13 +15,16 @@ package org.openhab.binding.hydrawise.internal;
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;
@ -40,14 +43,15 @@ import org.osgi.service.component.annotations.Reference;
@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();
}
@ -60,8 +64,12 @@ public class HydrawiseHandlerFactory extends BaseThingHandlerFactory {
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)) {

View File

@ -1,91 +0,0 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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();
}
}

View File

@ -12,12 +12,23 @@
*/
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);
}
}

View File

@ -1,312 +0,0 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}
}
}

View File

@ -12,13 +12,17 @@
*/
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);
}

View File

@ -12,13 +12,16 @@
*/
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);

View File

@ -0,0 +1,341 @@
/**
* 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);
}
}
}

View File

@ -0,0 +1,29 @@
/**
* 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();
}
}

View File

@ -0,0 +1,29 @@
/**
* 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;
}

View File

@ -0,0 +1,26 @@
/**
* 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;
}

View File

@ -0,0 +1,21 @@
/**
* 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;
}

View File

@ -0,0 +1,24 @@
/**
* 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;
}

View File

@ -0,0 +1,20 @@
/**
* 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;
}

View File

@ -0,0 +1,29 @@
/**
* 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;
}

View File

@ -0,0 +1,22 @@
/**
* 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;
}

View File

@ -0,0 +1,21 @@
/**
* 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;
}

View File

@ -0,0 +1,23 @@
/**
* 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;
}

View File

@ -0,0 +1,26 @@
/**
* 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);
}
}

View File

@ -0,0 +1,32 @@
/**
* 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;
}
}

View File

@ -0,0 +1,20 @@
/**
* 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;
}

View File

@ -0,0 +1,24 @@
/**
* 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;
}
}

View File

@ -0,0 +1,23 @@
/**
* 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;
}

View File

@ -0,0 +1,21 @@
/**
* 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;
}

View File

@ -0,0 +1,20 @@
/**
* 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;
}

View File

@ -0,0 +1,22 @@
/**
* 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;
}

View File

@ -0,0 +1,24 @@
/**
* 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;
}

View File

@ -0,0 +1,24 @@
/**
* 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;
}

View File

@ -0,0 +1,21 @@
/**
* 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;
}

View File

@ -0,0 +1,20 @@
/**
* 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;
}

View File

@ -0,0 +1,21 @@
/**
* 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;
}

View File

@ -0,0 +1,26 @@
/**
* 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;
}

View File

@ -0,0 +1,21 @@
/**
* 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;
}

View File

@ -0,0 +1,23 @@
/**
* 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;
}

View File

@ -0,0 +1,20 @@
/**
* 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;
}

View File

@ -10,22 +10,25 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api;
package org.openhab.binding.hydrawise.internal.api.local;
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.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.model.LocalScheduleResponse;
import org.openhab.binding.hydrawise.internal.api.model.SetZoneResponse;
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;
@ -92,11 +95,12 @@ public class HydrawiseLocalApiClient {
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
*/
@Nullable
public LocalScheduleResponse getLocalSchedule()
throws HydrawiseConnectionException, HydrawiseAuthenticationException {
String json = doGet(localGetURL);
LocalScheduleResponse response = gson.fromJson(json, LocalScheduleResponse.class);
return Objects.requireNonNull(response);
return response;
}
/**

View File

@ -10,7 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api;
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
@ -19,6 +21,7 @@ package org.openhab.binding.hydrawise.internal.api;
* @author Dan Cunningham - Initial contribution
*
*/
@NonNullByDefault
class HydrawiseZoneCommandBuilder {
private final StringBuilder builder;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.List;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.List;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.List;
@ -59,7 +59,5 @@ public class Controller {
public String statusIcon;
public Boolean online;
public List<String> tags = null;
}

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.List;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.List;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
/**
* The {@link Forecast} class models a daily weather forecast

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.LinkedList;
import java.util.List;
@ -22,9 +22,9 @@ import java.util.List;
*/
public class LocalScheduleResponse extends Response {
public List<Running> running = new LinkedList<>();
public List<Running> running = new LinkedList<Running>();
public List<Relay> relays = new LinkedList<>();
public List<Relay> relays = new LinkedList<Relay>();
public String name;

View File

@ -10,7 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link PlanArray} class models am account plan.
@ -63,7 +65,8 @@ public class PlanArray {
public String filetypeall;
public String plan_type;
@SerializedName(value = "plan_type")
public String planType;
public String pushNotification;

View File

@ -10,9 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
import com.google.gson.annotations.SerializedName;
package org.openhab.binding.hydrawise.internal.api.local.dto;
/**
* The {@link Relay} class models the Relay response message
@ -23,27 +21,19 @@ 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;
public Integer relay;
@SerializedName("run_seconds")
public Integer runTimeSeconds;
public String name;
public String nicetime;
public Integer frequency;
public String id;
public String timestr;
public Integer runSeconds;
/**
* Returns back the actual relay number when multiple controllers are chained.

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
/**
* The {@link Response} class models Response messages

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
/**
* The {@link Running} class models a running relay

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.List;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
/**
* The {@link SetControllerResponse} class models the SetController response message

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
/**
* The {@link SetZoneResponse} class models the SetZone response message

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.LinkedList;
import java.util.List;
@ -30,7 +30,7 @@ public class StatusScheduleResponse extends LocalScheduleResponse {
public Integer nextpoll;
public List<Sensor> sensors = new LinkedList<>();
public List<Sensor> sensors = new LinkedList<Sensor>();
public String message;
@ -52,7 +52,7 @@ public class StatusScheduleResponse extends LocalScheduleResponse {
public String lastContact;
public List<Forecast> forecast = new LinkedList<>();
public List<Forecast> forecast = new LinkedList<Forecast>();
public String status;

View File

@ -0,0 +1,28 @@
/**
* 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;
}

View File

@ -10,27 +10,19 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal;
package org.openhab.binding.hydrawise.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link HydrawiseCloudConfiguration} class contains fields mapping thing configuration parameters.
* The {@link HydrawiseControllerConfiguration} 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;
@NonNullByDefault
public class HydrawiseControllerConfiguration {
/**
* optional id of the controller to connect to
*/
public Integer controllerId;
public Integer controllerId = -1;
}

View File

@ -10,30 +10,31 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal;
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;
public String host = "";
/**
* User name (admin) for local controller
*/
public String username;
public String username = "";
/**
* Password for local controller
*/
public String password;
public String password = "";
/**
* refresh interval in seconds.
*/
public int refresh;
public int refresh = 30;
}

View File

@ -0,0 +1,110 @@
/**
* 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());
}
}
}

View File

@ -0,0 +1,216 @@
/**
* 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();
}
}
}

View File

@ -0,0 +1,436 @@
/**
* 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);
}
}

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal;
package org.openhab.binding.hydrawise.internal.handler;
import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
@ -19,23 +19,27 @@ 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.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.model.LocalScheduleResponse;
import org.openhab.binding.hydrawise.internal.api.model.Relay;
import org.openhab.binding.hydrawise.internal.api.model.Running;
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;
@ -49,23 +53,22 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HydrawiseHandler} is responsible for handling commands, which are
* The {@link HydrawiseLocalHandler} 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);
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;
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;
private static final long MAX_RUN_TIME = 157680000;
/**
* Minimum amount of time we can poll for updates
@ -86,8 +89,28 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
* Future to poll for updated
*/
public HydrawiseHandler(Thing thing) {
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
@ -120,15 +143,14 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
clearPolling();
switch (channelId) {
case CHANNEL_ZONE_RUN_CUSTOM:
if (!(command instanceof DecimalType)) {
if (!(command instanceof QuantityType<?>)) {
logger.warn("Invalid command type for run custom {}", command.getClass().getName());
return;
}
if (allCommand) {
sendRunAllCommand(((DecimalType) command).intValue());
client.runAllRelays(((QuantityType<?>) command).intValue());
} else {
Objects.requireNonNull(relay);
sendRunCommand(((DecimalType) command).intValue(), relay);
client.runRelay(((QuantityType<?>) command).intValue(), relay.relay);
}
break;
case CHANNEL_ZONE_RUN:
@ -138,16 +160,15 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
}
if (allCommand) {
if (command == OnOffType.ON) {
sendRunAllCommand();
client.runAllRelays();
} else {
sendStopAllCommand();
client.stopAllRelays();
}
} else {
Objects.requireNonNull(relay);
if (command == OnOffType.ON) {
sendRunCommand(relay);
client.runRelay(relay.relay);
} else {
sendStopCommand(relay);
client.stopRelay(relay.relay);
}
}
break;
@ -163,46 +184,6 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
}
}
@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 -> {
@ -211,12 +192,8 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
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));
}
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 {
@ -228,32 +205,21 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
.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));
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 DecimalType(0));
updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new QuantityType<>(0, Units.SECOND));
}
updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_RUN,
!status.running.isEmpty() ? OnOffType.ON : OnOffType.OFF);
status.running.size() > 0 ? 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);
@ -269,11 +235,19 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
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());
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());
@ -310,7 +284,10 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
*/
private void pollControllerInternal() {
try {
pollController();
LocalScheduleResponse response = client.getLocalSchedule();
if (response != null) {
updateZones(response);
}
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
@ -324,4 +301,14 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
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);
}
}
}

View File

@ -10,12 +10,14 @@
<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>
@ -25,6 +27,8 @@
<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>
@ -34,11 +38,11 @@
<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>
@ -49,13 +53,43 @@
<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>
@ -70,10 +104,17 @@
<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>
@ -87,7 +128,7 @@
<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>
@ -98,13 +139,25 @@
</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>
@ -126,22 +179,15 @@
<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>
@ -160,6 +206,7 @@
<description>Sensor off level</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="active">
<item-type>Switch</item-type>
<label>Active</label>
@ -167,6 +214,13 @@
<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>
@ -198,10 +252,10 @@
<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>
@ -220,4 +274,25 @@
<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>

View File

@ -4,14 +4,50 @@
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>
@ -29,6 +65,8 @@
<description>Sensor 4</description>
</channel-group>
<!-- Forecasts -->
<channel-group id="forecast1" typeId="forecast">
<label>Today's Weather</label>
<description>Today's weather forecast</description>
@ -41,13 +79,13 @@
<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>
@ -194,19 +232,9 @@
</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>
@ -214,7 +242,7 @@
<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>
@ -383,6 +411,4 @@
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,116 @@
{
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
}
}
}
}
}