]> git.basschouten.com Git - openhab-addons.git/commitdiff
[hydrawise] Migrate to new GraphQL based API (#10947)
authorDan Cunningham <dan@digitaldan.com>
Sun, 1 Aug 2021 18:03:37 +0000 (11:03 -0700)
committerGitHub <noreply@github.com>
Sun, 1 Aug 2021 18:03:37 +0000 (20:03 +0200)
* [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>
85 files changed:
bundles/org.openhab.binding.hydrawise/README.md
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseBindingConstants.java
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseCloudConfiguration.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseCloudHandler.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseControllerListener.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseHandler.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseHandlerFactory.java
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseLocalConfiguration.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseLocalHandler.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseAuthenticationException.java
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseCloudApiClient.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseCommandException.java
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseConnectionException.java
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseLocalApiClient.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseZoneCommandBuilder.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/HydrawiseGraphQLClient.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/AuthToken.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Controller.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ControllerStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Coordinates.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Customer.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Data.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Forecast.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Icon.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Input.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Location.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Mutation.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/MutationResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/PastRuns.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponseError.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponseErrorExtensions.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ScheduledRuns.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Sensor.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/SensorModel.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/SensorStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Time.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/UnitValue.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Zone.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneNumber.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneRun.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/HydrawiseLocalApiClient.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/HydrawiseZoneCommandBuilder.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/BocTopologyActual.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/BocTopologyDesired.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Controller.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/CustomerDetailsResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Features.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Forecast.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/LocalScheduleResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/PlanArray.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Relay.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Response.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Running.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Sensor.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/SetControllerResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/SetZoneResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/StatusScheduleResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/BocTopologyActual.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/BocTopologyDesired.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Controller.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/CustomerDetailsResponse.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Features.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Forecast.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/LocalScheduleResponse.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/PlanArray.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Relay.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Response.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Running.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Sensor.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/SetControllerResponse.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/SetZoneResponse.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/StatusScheduleResponse.java [deleted file]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/config/HydrawiseAccountConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/config/HydrawiseControllerConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/config/HydrawiseLocalConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/discovery/HydrawiseCloudControllerDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseAccountHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseControllerHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseLocalHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.hydrawise/src/main/resources/OH-INF/thing/channel-types.xml
bundles/org.openhab.binding.hydrawise/src/main/resources/OH-INF/thing/things.xml
bundles/org.openhab.binding.hydrawise/src/main/resources/query.graphql [new file with mode: 0644]

index dfa54679cf444b5e048bf4cc656ad0b2744888da..33df7cb438367ee1338ceaeb869917a63e7ac8e3 100644 (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
-
-| Configuration Name | type    | required | Comments                                                                           |
-|--------------------|---------|----------|------------------------------------------------------------------------------------|
-| apiKey             | String  | True     |                                                                                    |
-| refresh            | Integer | True     | Defaults to a 30 seconds polling rate                                              |
-| controllerId       | Integer | False    | Optional id of the controller if you have more then one registered to your account |
+### Account Thing
 
-To obtain your API key, log into your [Hydrawsie Account](https://app.hydrawise.com/config/login) and click on your account icon, then account details:
+| Configuration Name | type    | required | Comments                                                                                                                 |
+|--------------------|---------|----------|--------------------------------------------------------------------------------------------------------------------------|
+| userName           | String  | False    | The Hydrawise account user name                                                                                          |
+| password           | String  | False    | The Hydrawise account password                                                                                           |
+| savePassword       | Boolean | False    | By default the password will be not be persisted after the first login attempt unless this is true, defaults to false    |
+| refresh            | Integer | False    | Defaults to a 60 second polling rate, more frequent polling may cause the service to deny requests                       |
+| refreshToken       | Boolean | False    | A oAuth refresh token, this will be automatically configured after the first login and updated as the token is refreshed |
 
-![Account](doc/settings.png)
+### Controller Thing
 
-Then copy the API key shown here:
+| Configuration Name | type    | required | Comments             |
+|--------------------|---------|----------|----------------------|
+| controllerId       | Integer | True     | ID of the controller |
 
-![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"}
 ```
 
index d578d4290ddd6b515794aff1ba425214c975294d..86071a804dc8aa64b4dd7d1d025440ece66f2182 100644 (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";
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseCloudConfiguration.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseCloudConfiguration.java
deleted file mode 100644 (file)
index 417497b..0000000
+++ /dev/null
@@ -1,36 +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;
-
-/**
- * The {@link HydrawiseCloudConfiguration} class contains fields mapping thing configuration parameters.
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class HydrawiseCloudConfiguration {
-
-    /**
-     * Customer API key {@link https://app.hydrawise.com/config/account}
-     */
-    public String apiKey;
-
-    /**
-     * refresh interval in seconds.
-     */
-    public Integer refresh;
-
-    /**
-     * optional id of the controller to connect to
-     */
-    public Integer controllerId;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseCloudHandler.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseCloudHandler.java
deleted file mode 100644 (file)
index fccc6a2..0000000
+++ /dev/null
@@ -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;
-    }
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseControllerListener.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseControllerListener.java
new file mode 100644 (file)
index 0000000..a546409
--- /dev/null
@@ -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);
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseHandler.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseHandler.java
deleted file mode 100644 (file)
index 932d888..0000000
+++ /dev/null
@@ -1,327 +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.time.ZonedDateTime;
-import java.time.temporal.ChronoUnit;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
-import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
-import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
-import org.openhab.binding.hydrawise.internal.api.model.LocalScheduleResponse;
-import org.openhab.binding.hydrawise.internal.api.model.Relay;
-import org.openhab.binding.hydrawise.internal.api.model.Running;
-import org.openhab.core.library.types.DateTimeType;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.StringType;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.thing.ThingStatus;
-import org.openhab.core.thing.ThingStatusDetail;
-import org.openhab.core.thing.binding.BaseThingHandler;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.openhab.core.types.State;
-import org.openhab.core.types.UnDefType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link HydrawiseHandler} is responsible for handling commands, which are
- * sent to one of the channels.
- *
- * @author Dan Cunningham - Initial contribution
- */
-@NonNullByDefault
-public abstract class HydrawiseHandler extends BaseThingHandler {
-
-    private final Logger logger = LoggerFactory.getLogger(HydrawiseHandler.class);
-    private @Nullable ScheduledFuture<?> pollFuture;
-    private Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<>());
-    private Map<String, Relay> relayMap = Collections.synchronizedMap(new HashMap<>());
-
-    /**
-     * value observed being used by the Hydrawise clients as a max time value,
-     */
-    private static long MAX_RUN_TIME = 157680000;
-
-    /**
-     * Minimum amount of time we can poll for updates
-     */
-    protected static final int MIN_REFRESH_SECONDS = 5;
-
-    /**
-     * Minimum amount of time we can poll after a command
-     */
-    protected static final int COMMAND_REFRESH_SECONDS = 5;
-
-    /**
-     * Our poll rate
-     */
-    protected int refresh;
-
-    /**
-     * Future to poll for updated
-     */
-
-    public HydrawiseHandler(Thing thing) {
-        super(thing);
-    }
-
-    @SuppressWarnings({ "null", "unused" }) // compiler does not like relayMap.get can return null
-    @Override
-    public void handleCommand(ChannelUID channelUID, Command command) {
-        if (getThing().getStatus() != ThingStatus.ONLINE) {
-            logger.warn("Controller is NOT ONLINE and is not responding to commands");
-            return;
-        }
-
-        // remove our cached state for this, will be safely updated on next poll
-        stateMap.remove(channelUID.getAsString());
-
-        if (command instanceof RefreshType) {
-            // we already removed this from the cache
-            return;
-        }
-
-        String group = channelUID.getGroupId();
-        String channelId = channelUID.getIdWithoutGroup();
-        boolean allCommand = CHANNEL_GROUP_ALLZONES.equals(group);
-
-        Relay relay = relayMap.get(group);
-        if (!allCommand && relay == null) {
-            logger.debug("Zone not found {}", group);
-            return;
-        }
-
-        try {
-            clearPolling();
-            switch (channelId) {
-                case CHANNEL_ZONE_RUN_CUSTOM:
-                    if (!(command instanceof DecimalType)) {
-                        logger.warn("Invalid command type for run custom {}", command.getClass().getName());
-                        return;
-                    }
-                    if (allCommand) {
-                        sendRunAllCommand(((DecimalType) command).intValue());
-                    } else {
-                        Objects.requireNonNull(relay);
-                        sendRunCommand(((DecimalType) command).intValue(), relay);
-                    }
-                    break;
-                case CHANNEL_ZONE_RUN:
-                    if (!(command instanceof OnOffType)) {
-                        logger.warn("Invalid command type for run {}", command.getClass().getName());
-                        return;
-                    }
-                    if (allCommand) {
-                        if (command == OnOffType.ON) {
-                            sendRunAllCommand();
-                        } else {
-                            sendStopAllCommand();
-                        }
-                    } else {
-                        Objects.requireNonNull(relay);
-                        if (command == OnOffType.ON) {
-                            sendRunCommand(relay);
-                        } else {
-                            sendStopCommand(relay);
-                        }
-                    }
-                    break;
-            }
-            initPolling(COMMAND_REFRESH_SECONDS);
-        } catch (HydrawiseCommandException | HydrawiseConnectionException e) {
-            logger.debug("Could not issue command", e);
-            initPolling(COMMAND_REFRESH_SECONDS);
-        } catch (HydrawiseAuthenticationException e) {
-            logger.debug("Credentials not valid");
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
-            configureInternal();
-        }
-    }
-
-    @Override
-    public void initialize() {
-        scheduler.schedule(this::configureInternal, 0, TimeUnit.SECONDS);
-    }
-
-    @Override
-    public void dispose() {
-        logger.debug("Handler disposed.");
-        clearPolling();
-    }
-
-    @Override
-    public void channelLinked(ChannelUID channelUID) {
-        // clear our cached value so the new channel gets updated on the next poll
-        stateMap.remove(channelUID.getId());
-    }
-
-    protected abstract void configure()
-            throws NotConfiguredException, HydrawiseConnectionException, HydrawiseAuthenticationException;
-
-    protected abstract void pollController() throws HydrawiseConnectionException, HydrawiseAuthenticationException;
-
-    protected abstract void sendRunCommand(int seconds, Relay relay)
-            throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
-
-    protected abstract void sendRunCommand(Relay relay)
-            throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
-
-    protected abstract void sendStopCommand(Relay relay)
-            throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
-
-    protected abstract void sendRunAllCommand()
-            throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
-
-    protected abstract void sendRunAllCommand(int seconds)
-            throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
-
-    protected abstract void sendStopAllCommand()
-            throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
-
-    protected void updateZones(LocalScheduleResponse status) {
-        ZonedDateTime now = ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS);
-        status.relays.forEach(r -> {
-            String group = "zone" + r.getRelayNumber();
-            relayMap.put(group, r);
-            logger.trace("Updateing Zone {} {} ", group, r.name);
-            updateGroupState(group, CHANNEL_ZONE_NAME, new StringType(r.name));
-            updateGroupState(group, CHANNEL_ZONE_TYPE, new DecimalType(r.type));
-            updateGroupState(group, CHANNEL_ZONE_TIME,
-                    r.runTimeSeconds != null ? new DecimalType(r.runTimeSeconds) : UnDefType.UNDEF);
-            String icon = r.icon;
-            if (icon != null && !icon.isBlank()) {
-                updateGroupState(group, CHANNEL_ZONE_ICON, new StringType(BASE_IMAGE_URL + icon));
-            }
-            if (r.time >= MAX_RUN_TIME) {
-                updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME, UnDefType.UNDEF);
-            } else {
-                updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME,
-                        new DateTimeType(now.plusSeconds(r.time).truncatedTo(ChronoUnit.MINUTES)));
-            }
-
-            Optional<Running> running = status.running.stream()
-                    .filter(z -> Integer.parseInt(z.relayId) == r.relayId.intValue()).findAny();
-            if (running.isPresent()) {
-                updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.ON);
-                updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(running.get().timeLeft));
-                logger.debug("{} Time Left {}", r.name, running.get().timeLeft);
-
-            } else {
-                updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.OFF);
-                updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(0));
-
-            }
-
-            updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_RUN,
-                    !status.running.isEmpty() ? OnOffType.ON : OnOffType.OFF);
-        });
-    }
-
-    protected void updateGroupState(String group, String channelID, State state) {
-        String channelName = group + "#" + channelID;
-        State oldState = stateMap.put(channelName, state);
-        if (!state.equals(oldState)) {
-            ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelName);
-            logger.debug("updateState updating {} {}", channelUID, state);
-            updateState(channelUID, state);
-        }
-    }
-
-    @SuppressWarnings("serial")
-    @NonNullByDefault
-    protected class NotConfiguredException extends Exception {
-        NotConfiguredException(String message) {
-            super(message);
-        }
-    }
-
-    private boolean isFutureValid(@Nullable ScheduledFuture<?> future) {
-        return future != null && !future.isCancelled();
-    }
-
-    private void configureInternal() {
-        clearPolling();
-        stateMap.clear();
-        relayMap.clear();
-        try {
-            configure();
-            initPolling(0);
-        } catch (NotConfiguredException e) {
-            logger.debug("Configuration error {}", e.getMessage());
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
-        } catch (HydrawiseConnectionException e) {
-            logger.debug("Could not connect to service");
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
-        } catch (HydrawiseAuthenticationException e) {
-            logger.debug("Credentials not valid");
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
-        }
-    }
-
-    /**
-     * Starts/Restarts polling with an initial delay. This allows changes in the poll cycle for when commands are sent
-     * and we need to poll sooner then the next refresh cycle.
-     */
-    private synchronized void initPolling(int initalDelay) {
-        clearPolling();
-        pollFuture = scheduler.scheduleWithFixedDelay(this::pollControllerInternal, initalDelay, refresh,
-                TimeUnit.SECONDS);
-    }
-
-    /**
-     * Stops/clears this thing's polling future
-     */
-    private void clearPolling() {
-        ScheduledFuture<?> localFuture = pollFuture;
-        if (isFutureValid(localFuture)) {
-            if (localFuture != null) {
-                localFuture.cancel(false);
-            }
-        }
-    }
-
-    /**
-     * Poll the controller for updates.
-     */
-    private void pollControllerInternal() {
-        try {
-            pollController();
-            if (getThing().getStatus() != ThingStatus.ONLINE) {
-                updateStatus(ThingStatus.ONLINE);
-            }
-        } catch (HydrawiseConnectionException e) {
-            // poller will continue to run, set offline until next run
-            logger.debug("Exception polling", e);
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
-        } catch (HydrawiseAuthenticationException e) {
-            // if are creds are not valid, we need to try re authorizing again
-            logger.debug("Authorization exception during polling", e);
-            configureInternal();
-        }
-    }
-}
index 6ab1265676788d0bc404ed0d58a12481c27b4269..5033f45e1ef752807780858ce709a14fd58cc3f6 100644 (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)) {
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseLocalConfiguration.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseLocalConfiguration.java
deleted file mode 100644 (file)
index a4b26a1..0000000
+++ /dev/null
@@ -1,39 +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;
-
-/**
- * The {@link HydrawiseLocalConfiguration} class contains fields mapping thing configuration parameters.
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class HydrawiseLocalConfiguration {
-
-    /**
-     * Host or IP for local controller
-     */
-    public String host;
-    /**
-     * User name (admin) for local controller
-     */
-    public String username;
-    /**
-     * Password for local controller
-     */
-    public String password;
-
-    /**
-     * refresh interval in seconds.
-     */
-    public int refresh;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseLocalHandler.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseLocalHandler.java
deleted file mode 100644 (file)
index 4752563..0000000
+++ /dev/null
@@ -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();
-    }
-}
index 2b6a42de6ef43cacf1121762f6ae7e21914d1503..c4ff72573a65bcc1bae4ae9dbca5490f933e79ef 100644 (file)
  */
 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);
+    }
 }
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseCloudApiClient.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseCloudApiClient.java
deleted file mode 100644 (file)
index dad4e42..0000000
+++ /dev/null
@@ -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);
-            }
-        }
-    }
-}
index d2d74b59013cf112c616fa7249d6e4c6d03201ee..742ae33efacd345cc0b89bdc853d847c7cfa70fb 100644 (file)
  */
 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);
     }
index b4835d1cd040e203999af7df815b3b8f108da8dc..cc221425a94eb585aad000d5a8a8c2885d08c140 100644 (file)
  */
 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);
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseLocalApiClient.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseLocalApiClient.java
deleted file mode 100644 (file)
index df4c5c3..0000000
+++ /dev/null
@@ -1,215 +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.net.URI;
-import java.util.Objects;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jetty.client.HttpClient;
-import org.eclipse.jetty.client.api.AuthenticationStore;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.util.BasicAuthentication;
-import org.eclipse.jetty.http.HttpMethod;
-import org.openhab.binding.hydrawise.internal.api.model.LocalScheduleResponse;
-import org.openhab.binding.hydrawise.internal.api.model.SetZoneResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.FieldNamingPolicy;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-
-/**
- * The {@link HydrawiseLocalApiClient} communicates with a network local Hydrawise controller.
- *
- * @author Dan Cunningham - Initial contribution
- */
-@NonNullByDefault
-public class HydrawiseLocalApiClient {
-    private final Logger logger = LoggerFactory.getLogger(HydrawiseLocalApiClient.class);
-
-    private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
-            .create();
-
-    private static final String GET_LOCAL_DATA_URL = "%s/get_sched_json.php?hours=720";
-    private static final String SET_LOCAL_DATA_URL = "%s/set_manual_data.php?period_id=998";
-
-    private static final int TIMEOUT_SECONDS = 30;
-    private HttpClient httpClient;
-    private String localSetURL = "";
-    private String localGetURL = "";
-
-    public HydrawiseLocalApiClient(HttpClient httpClient) {
-        this.httpClient = httpClient;
-    }
-
-    /**
-     * Initializes the {@link HydrawiseLocalApiClient} to talk with the network local Hydrawise API
-     *
-     * @param host
-     * @param username
-     * @param password
-     */
-    public HydrawiseLocalApiClient(String host, String username, String password, HttpClient httpClient) {
-        this.httpClient = httpClient;
-        setCredentials(host, username, password);
-    }
-
-    /**
-     * Sets the local credentials and controller host
-     *
-     * @param host
-     * @param username
-     * @param password
-     */
-    public void setCredentials(String host, String username, String password) {
-        String url = "http://" + host;
-        localSetURL = String.format(SET_LOCAL_DATA_URL, url);
-        localGetURL = String.format(GET_LOCAL_DATA_URL, url);
-        AuthenticationStore auth = httpClient.getAuthenticationStore();
-        URI uri = URI.create(url);
-        auth.addAuthenticationResult(new BasicAuthentication.BasicResult(uri, username, password));
-    }
-
-    /**
-     * Retrieves the {@link LocalScheduleResponse} for the controller
-     *
-     * @return the local schedule response
-     * @throws HydrawiseConnectionException
-     * @throws HydrawiseAuthenticationException
-     */
-    public LocalScheduleResponse getLocalSchedule()
-            throws HydrawiseConnectionException, HydrawiseAuthenticationException {
-        String json = doGet(localGetURL);
-        LocalScheduleResponse response = gson.fromJson(json, LocalScheduleResponse.class);
-        return Objects.requireNonNull(response);
-    }
-
-    /**
-     * Stops a given relay
-     *
-     * @param number
-     * @return Response message
-     * @throws HydrawiseConnectionException
-     * @throws HydrawiseAuthenticationException
-     * @throws HydrawiseCommandException
-     */
-    public String stopRelay(int number)
-            throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
-        return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("stop").relayNumber(number).toString());
-    }
-
-    /**
-     * Runs a given relay for the default amount of time
-     *
-     * @param number
-     * @return Response message
-     * @throws HydrawiseConnectionException
-     * @throws HydrawiseAuthenticationException
-     * @throws HydrawiseCommandException
-     */
-    public String runRelay(int number)
-            throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
-        return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("run").relayNumber(number).toString());
-    }
-
-    /**
-     * Runs a given relay for a specified numbers of seconds
-     *
-     * @param seconds
-     * @param number
-     * @return Response message
-     * @throws HydrawiseConnectionException
-     * @throws HydrawiseAuthenticationException
-     * @throws HydrawiseCommandException
-     */
-    public String runRelay(int seconds, int number)
-            throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
-        return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("run").relayNumber(number)
-                .duration(seconds).toString());
-    }
-
-    /**
-     * Stops all relays
-     *
-     * @return Response message
-     * @throws HydrawiseConnectionException
-     * @throws HydrawiseAuthenticationException
-     * @throws HydrawiseCommandException
-     */
-    public String stopAllRelays()
-            throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
-        return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("stopall").toString());
-    }
-
-    /**
-     * Run all relays for the default amount of time
-     *
-     * @return Response message
-     * @throws HydrawiseConnectionException
-     * @throws HydrawiseAuthenticationException
-     * @throws HydrawiseCommandException
-     */
-    public String runAllRelays()
-            throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
-        return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("runall").toString());
-    }
-
-    /**
-     * Run all relays for a given amount of seconds
-     *
-     * @param seconds
-     * @return Response message
-     * @throws HydrawiseConnectionException
-     * @throws HydrawiseAuthenticationException
-     * @throws HydrawiseCommandException
-     */
-    public String runAllRelays(int seconds)
-            throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
-        return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("runall").duration(seconds).toString());
-    }
-
-    private String relayCommand(String url)
-            throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
-        String json = doGet(url);
-        SetZoneResponse response = gson.fromJson(json, SetZoneResponse.class);
-        if (response.messageType.equals("error")) {
-            throw new HydrawiseCommandException(response.message);
-        }
-        return response.message;
-    }
-
-    private String doGet(String url) throws HydrawiseConnectionException, HydrawiseAuthenticationException {
-        logger.trace("Getting {}", url);
-        ContentResponse response;
-        try {
-            response = httpClient.newRequest(url).method(HttpMethod.GET).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
-                    .send();
-        } catch (InterruptedException | TimeoutException | ExecutionException e) {
-            throw new HydrawiseConnectionException(e);
-        }
-        if (response.getStatus() == 401) {
-            throw new HydrawiseAuthenticationException();
-        }
-        if (response.getStatus() != 200) {
-            throw new HydrawiseConnectionException("Error from controller.  Response code " + response.getStatus());
-        }
-        String stringResponse = response.getContentAsString();
-        logger.trace("Response: {}", stringResponse);
-        return stringResponse;
-    }
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseZoneCommandBuilder.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseZoneCommandBuilder.java
deleted file mode 100644 (file)
index 4aafec3..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-/**
- * Copyright (c) 2010-2021 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.hydrawise.internal.api;
-
-/**
- * The {@link HydrawiseZoneCommandBuilder} class builds a command URL string to use when sending commands to the
- * Hydrawise local controller or cloud based API server
- *
- * @author Dan Cunningham - Initial contribution
- *
- */
-class HydrawiseZoneCommandBuilder {
-
-    private final StringBuilder builder;
-
-    /**
-     * Construct a new {@link HydrawiseZoneCommandBuilder} class with a base URL
-     *
-     * @param baseURL
-     */
-    public HydrawiseZoneCommandBuilder(String baseURL) {
-        builder = new StringBuilder(baseURL);
-    }
-
-    /**
-     * Construct a new {@link HydrawiseZoneCommandBuilder} class with a base URL and API key.
-     *
-     * @param baseURL
-     * @param apiKey
-     */
-    public HydrawiseZoneCommandBuilder(String baseURL, String apiKey) {
-        this(baseURL);
-        builder.append("&api_key=" + apiKey);
-    }
-
-    /**
-     * Sets the action parameter
-     *
-     * @param action
-     * @return {@link HydrawiseZoneCommandBuilder}
-     */
-    public HydrawiseZoneCommandBuilder action(String action) {
-        builder.append("&action=" + action);
-        return this;
-    }
-
-    /**
-     * Sets the relayId parameter
-     *
-     * @param action
-     * @return {@link HydrawiseZoneCommandBuilder}
-     */
-    public HydrawiseZoneCommandBuilder relayId(int relayId) {
-        builder.append("&relay_id=" + relayId);
-        return this;
-    }
-
-    /**
-     * Sets the relay number parameter
-     *
-     * @param action
-     * @return {@link HydrawiseZoneCommandBuilder}
-     */
-    public HydrawiseZoneCommandBuilder relayNumber(int number) {
-        builder.append("&relay=" + number);
-        return this;
-    }
-
-    /**
-     * Sets the run duration parameter
-     *
-     * @param action
-     * @return {@link HydrawiseZoneCommandBuilder}
-     */
-    public HydrawiseZoneCommandBuilder duration(int seconds) {
-        builder.append("&custom=" + seconds);
-        return this;
-    }
-
-    /**
-     * Sets the controller Id parameter
-     *
-     * @param action
-     * @return {@link HydrawiseZoneCommandBuilder}
-     */
-    public HydrawiseZoneCommandBuilder controllerId(int controllerId) {
-        builder.append("&controller_id=" + controllerId);
-        return this;
-    }
-
-    @Override
-    public String toString() {
-        return builder.toString();
-    }
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/HydrawiseGraphQLClient.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/HydrawiseGraphQLClient.java
new file mode 100644 (file)
index 0000000..9cd80fc
--- /dev/null
@@ -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);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/AuthToken.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/AuthToken.java
new file mode 100644 (file)
index 0000000..029df29
--- /dev/null
@@ -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();
+    }
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Controller.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Controller.java
new file mode 100644 (file)
index 0000000..02d260a
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ControllerStatus.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ControllerStatus.java
new file mode 100644 (file)
index 0000000..f9b15b5
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Coordinates.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Coordinates.java
new file mode 100644 (file)
index 0000000..0582966
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Customer.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Customer.java
new file mode 100644 (file)
index 0000000..ca7ce63
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Data.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Data.java
new file mode 100644 (file)
index 0000000..941255b
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Forecast.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Forecast.java
new file mode 100644 (file)
index 0000000..1b3803b
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Icon.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Icon.java
new file mode 100644 (file)
index 0000000..2853613
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Input.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Input.java
new file mode 100644 (file)
index 0000000..aaf9df7
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Location.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Location.java
new file mode 100644 (file)
index 0000000..c3023ec
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Mutation.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Mutation.java
new file mode 100644 (file)
index 0000000..d518a80
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/MutationResponse.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/MutationResponse.java
new file mode 100644 (file)
index 0000000..b7908c1
--- /dev/null
@@ -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;
+    }
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/PastRuns.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/PastRuns.java
new file mode 100644 (file)
index 0000000..dea3bf5
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryRequest.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryRequest.java
new file mode 100644 (file)
index 0000000..5b2d4a0
--- /dev/null
@@ -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;
+    }
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponse.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponse.java
new file mode 100644 (file)
index 0000000..49ad458
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponseError.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponseError.java
new file mode 100644 (file)
index 0000000..771ac42
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponseErrorExtensions.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponseErrorExtensions.java
new file mode 100644 (file)
index 0000000..fbf6c21
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ScheduledRuns.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ScheduledRuns.java
new file mode 100644 (file)
index 0000000..768a439
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Sensor.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Sensor.java
new file mode 100644 (file)
index 0000000..2b780f4
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/SensorModel.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/SensorModel.java
new file mode 100644 (file)
index 0000000..dd9606c
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/SensorStatus.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/SensorStatus.java
new file mode 100644 (file)
index 0000000..7176b39
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Time.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Time.java
new file mode 100644 (file)
index 0000000..125e520
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/UnitValue.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/UnitValue.java
new file mode 100644 (file)
index 0000000..07c705d
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Zone.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Zone.java
new file mode 100644 (file)
index 0000000..46e037a
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneNumber.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneNumber.java
new file mode 100644 (file)
index 0000000..28c1764
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneRun.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneRun.java
new file mode 100644 (file)
index 0000000..e2030a7
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneStatus.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneStatus.java
new file mode 100644 (file)
index 0000000..4e66255
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/HydrawiseLocalApiClient.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/HydrawiseLocalApiClient.java
new file mode 100644 (file)
index 0000000..e68bd75
--- /dev/null
@@ -0,0 +1,219 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.api.local;
+
+import java.net.URI;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.AuthenticationStore;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.util.BasicAuthentication;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
+import org.openhab.binding.hydrawise.internal.api.local.dto.LocalScheduleResponse;
+import org.openhab.binding.hydrawise.internal.api.local.dto.SetZoneResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * The {@link HydrawiseLocalApiClient} communicates with a network local Hydrawise controller.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class HydrawiseLocalApiClient {
+    private final Logger logger = LoggerFactory.getLogger(HydrawiseLocalApiClient.class);
+
+    private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+            .create();
+
+    private static final String GET_LOCAL_DATA_URL = "%s/get_sched_json.php?hours=720";
+    private static final String SET_LOCAL_DATA_URL = "%s/set_manual_data.php?period_id=998";
+
+    private static final int TIMEOUT_SECONDS = 30;
+    private HttpClient httpClient;
+    private String localSetURL = "";
+    private String localGetURL = "";
+
+    public HydrawiseLocalApiClient(HttpClient httpClient) {
+        this.httpClient = httpClient;
+    }
+
+    /**
+     * Initializes the {@link HydrawiseLocalApiClient} to talk with the network local Hydrawise API
+     *
+     * @param host
+     * @param username
+     * @param password
+     */
+    public HydrawiseLocalApiClient(String host, String username, String password, HttpClient httpClient) {
+        this.httpClient = httpClient;
+        setCredentials(host, username, password);
+    }
+
+    /**
+     * Sets the local credentials and controller host
+     *
+     * @param host
+     * @param username
+     * @param password
+     */
+    public void setCredentials(String host, String username, String password) {
+        String url = "http://" + host;
+        localSetURL = String.format(SET_LOCAL_DATA_URL, url);
+        localGetURL = String.format(GET_LOCAL_DATA_URL, url);
+        AuthenticationStore auth = httpClient.getAuthenticationStore();
+        URI uri = URI.create(url);
+        auth.addAuthenticationResult(new BasicAuthentication.BasicResult(uri, username, password));
+    }
+
+    /**
+     * Retrieves the {@link LocalScheduleResponse} for the controller
+     *
+     * @return the local schedule response
+     * @throws HydrawiseConnectionException
+     * @throws HydrawiseAuthenticationException
+     */
+    @Nullable
+    public LocalScheduleResponse getLocalSchedule()
+            throws HydrawiseConnectionException, HydrawiseAuthenticationException {
+        String json = doGet(localGetURL);
+        LocalScheduleResponse response = gson.fromJson(json, LocalScheduleResponse.class);
+        return response;
+    }
+
+    /**
+     * Stops a given relay
+     *
+     * @param number
+     * @return Response message
+     * @throws HydrawiseConnectionException
+     * @throws HydrawiseAuthenticationException
+     * @throws HydrawiseCommandException
+     */
+    public String stopRelay(int number)
+            throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+        return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("stop").relayNumber(number).toString());
+    }
+
+    /**
+     * Runs a given relay for the default amount of time
+     *
+     * @param number
+     * @return Response message
+     * @throws HydrawiseConnectionException
+     * @throws HydrawiseAuthenticationException
+     * @throws HydrawiseCommandException
+     */
+    public String runRelay(int number)
+            throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+        return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("run").relayNumber(number).toString());
+    }
+
+    /**
+     * Runs a given relay for a specified numbers of seconds
+     *
+     * @param seconds
+     * @param number
+     * @return Response message
+     * @throws HydrawiseConnectionException
+     * @throws HydrawiseAuthenticationException
+     * @throws HydrawiseCommandException
+     */
+    public String runRelay(int seconds, int number)
+            throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+        return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("run").relayNumber(number)
+                .duration(seconds).toString());
+    }
+
+    /**
+     * Stops all relays
+     *
+     * @return Response message
+     * @throws HydrawiseConnectionException
+     * @throws HydrawiseAuthenticationException
+     * @throws HydrawiseCommandException
+     */
+    public String stopAllRelays()
+            throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+        return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("stopall").toString());
+    }
+
+    /**
+     * Run all relays for the default amount of time
+     *
+     * @return Response message
+     * @throws HydrawiseConnectionException
+     * @throws HydrawiseAuthenticationException
+     * @throws HydrawiseCommandException
+     */
+    public String runAllRelays()
+            throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+        return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("runall").toString());
+    }
+
+    /**
+     * Run all relays for a given amount of seconds
+     *
+     * @param seconds
+     * @return Response message
+     * @throws HydrawiseConnectionException
+     * @throws HydrawiseAuthenticationException
+     * @throws HydrawiseCommandException
+     */
+    public String runAllRelays(int seconds)
+            throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+        return relayCommand(new HydrawiseZoneCommandBuilder(localSetURL).action("runall").duration(seconds).toString());
+    }
+
+    private String relayCommand(String url)
+            throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
+        String json = doGet(url);
+        SetZoneResponse response = gson.fromJson(json, SetZoneResponse.class);
+        if (response.messageType.equals("error")) {
+            throw new HydrawiseCommandException(response.message);
+        }
+        return response.message;
+    }
+
+    private String doGet(String url) throws HydrawiseConnectionException, HydrawiseAuthenticationException {
+        logger.trace("Getting {}", url);
+        ContentResponse response;
+        try {
+            response = httpClient.newRequest(url).method(HttpMethod.GET).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+                    .send();
+        } catch (InterruptedException | TimeoutException | ExecutionException e) {
+            throw new HydrawiseConnectionException(e);
+        }
+        if (response.getStatus() == 401) {
+            throw new HydrawiseAuthenticationException();
+        }
+        if (response.getStatus() != 200) {
+            throw new HydrawiseConnectionException("Error from controller.  Response code " + response.getStatus());
+        }
+        String stringResponse = response.getContentAsString();
+        logger.trace("Response: {}", stringResponse);
+        return stringResponse;
+    }
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/HydrawiseZoneCommandBuilder.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/HydrawiseZoneCommandBuilder.java
new file mode 100644 (file)
index 0000000..4f47964
--- /dev/null
@@ -0,0 +1,108 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.api.local;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link HydrawiseZoneCommandBuilder} class builds a command URL string to use when sending commands to the
+ * Hydrawise local controller or cloud based API server
+ *
+ * @author Dan Cunningham - Initial contribution
+ *
+ */
+@NonNullByDefault
+class HydrawiseZoneCommandBuilder {
+
+    private final StringBuilder builder;
+
+    /**
+     * Construct a new {@link HydrawiseZoneCommandBuilder} class with a base URL
+     *
+     * @param baseURL
+     */
+    public HydrawiseZoneCommandBuilder(String baseURL) {
+        builder = new StringBuilder(baseURL);
+    }
+
+    /**
+     * Construct a new {@link HydrawiseZoneCommandBuilder} class with a base URL and API key.
+     *
+     * @param baseURL
+     * @param apiKey
+     */
+    public HydrawiseZoneCommandBuilder(String baseURL, String apiKey) {
+        this(baseURL);
+        builder.append("&api_key=" + apiKey);
+    }
+
+    /**
+     * Sets the action parameter
+     *
+     * @param action
+     * @return {@link HydrawiseZoneCommandBuilder}
+     */
+    public HydrawiseZoneCommandBuilder action(String action) {
+        builder.append("&action=" + action);
+        return this;
+    }
+
+    /**
+     * Sets the relayId parameter
+     *
+     * @param action
+     * @return {@link HydrawiseZoneCommandBuilder}
+     */
+    public HydrawiseZoneCommandBuilder relayId(int relayId) {
+        builder.append("&relay_id=" + relayId);
+        return this;
+    }
+
+    /**
+     * Sets the relay number parameter
+     *
+     * @param action
+     * @return {@link HydrawiseZoneCommandBuilder}
+     */
+    public HydrawiseZoneCommandBuilder relayNumber(int number) {
+        builder.append("&relay=" + number);
+        return this;
+    }
+
+    /**
+     * Sets the run duration parameter
+     *
+     * @param action
+     * @return {@link HydrawiseZoneCommandBuilder}
+     */
+    public HydrawiseZoneCommandBuilder duration(int seconds) {
+        builder.append("&custom=" + seconds);
+        return this;
+    }
+
+    /**
+     * Sets the controller Id parameter
+     *
+     * @param action
+     * @return {@link HydrawiseZoneCommandBuilder}
+     */
+    public HydrawiseZoneCommandBuilder controllerId(int controllerId) {
+        builder.append("&controller_id=" + controllerId);
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return builder.toString();
+    }
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/BocTopologyActual.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/BocTopologyActual.java
new file mode 100644 (file)
index 0000000..96a980d
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.api.local.dto;
+
+import java.util.List;
+
+/**
+ * The {@link BocTopologyDesired} class models the actual BocTopology
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class BocTopologyActual {
+
+    public List<Object> bocGateways;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/BocTopologyDesired.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/BocTopologyDesired.java
new file mode 100644 (file)
index 0000000..f03460a
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.api.local.dto;
+
+import java.util.List;
+
+/**
+ * The {@link BocTopologyDesired} class models the desired BocTopology
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class BocTopologyDesired {
+
+    public List<Object> bocGateways;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Controller.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Controller.java
new file mode 100644 (file)
index 0000000..500cac8
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.api.local.dto;
+
+import java.util.List;
+
+/**
+ * The {@link Controller} class models a Hydrawise controller unit
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Controller {
+
+    public String name;
+
+    public Integer lastContact;
+
+    public String serialNumber;
+
+    public Integer controllerId;
+
+    public String swVersion;
+
+    public String hardware;
+
+    public Boolean isBoc;
+
+    public String address;
+
+    public String timezone;
+
+    public Integer deviceId;
+
+    public Object parentDeviceId;
+
+    public String image;
+
+    public String description;
+
+    public Integer customerId;
+
+    public Double latitude;
+
+    public Double longitude;
+
+    public String lastContactReadable;
+
+    public String status;
+
+    public String statusIcon;
+
+    public List<String> tags = null;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/CustomerDetailsResponse.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/CustomerDetailsResponse.java
new file mode 100644 (file)
index 0000000..6f45014
--- /dev/null
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.api.local.dto;
+
+import java.util.List;
+
+/**
+ * The {@link CustomerDetailsResponse} class models the CustomerDetails response message
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class CustomerDetailsResponse extends Response {
+
+    public BocTopologyDesired bocTopologyDesired;
+
+    public BocTopologyActual bocTopologyActual;
+
+    public List<Controller> controllers;
+
+    public String currentController;
+
+    public Boolean isBoc;
+
+    public Integer tandc;
+
+    public Integer controllerId;
+
+    public Integer customerId;
+
+    public String sessionId;
+
+    public String hardwareVersion;
+
+    public Integer deviceId;
+
+    public Integer tandcVersion;
+
+    public Features features;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Features.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Features.java
new file mode 100644 (file)
index 0000000..56b7d84
--- /dev/null
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.api.local.dto;
+
+import java.util.List;
+
+/**
+ * The {@link Features} class models an accounts features.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Features {
+
+    public List<PlanArray> planArray = null;
+
+    public Object id;
+
+    public String planType2;
+
+    public String planType2Key;
+
+    public Object sku;
+
+    public String discount;
+
+    public String cost;
+
+    public String costUs;
+
+    public String costAu;
+
+    public String costEu;
+
+    public String costCa;
+
+    public String costUk;
+
+    public String active;
+
+    public String controllerQty;
+
+    public String rainfall;
+
+    public String smsQty;
+
+    public String scheduledReports;
+
+    public String emailAlerts;
+
+    public String defineSensor;
+
+    public String addUser;
+
+    public String contractor;
+
+    public Object description;
+
+    public String sensorPack;
+
+    public String filelimit;
+
+    public String filetypeall;
+
+    public String planType;
+
+    public String pushNotification;
+
+    public String weatherQty;
+
+    public String weatherFreeQty;
+
+    public String reportingDays;
+
+    public String weatherHourlyUpdates;
+
+    public String freeEnthusiastPlans;
+
+    public String visible;
+
+    public Object contractorPurchasable;
+
+    public Integer boc;
+
+    public Object expiry;
+
+    public Object start;
+
+    public String customerplanId;
+
+    public Integer smsUsed;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Forecast.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Forecast.java
new file mode 100644 (file)
index 0000000..256628b
--- /dev/null
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.api.local.dto;
+
+/**
+ * The {@link Forecast} class models a daily weather forecast
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Forecast {
+
+    public String tempHi;
+
+    public String tempLo;
+
+    public String conditions;
+
+    public String day;
+
+    public Integer pop;
+
+    public Integer humidity;
+
+    public String wind;
+
+    public String icon;
+
+    public String iconLocal;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/LocalScheduleResponse.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/LocalScheduleResponse.java
new file mode 100644 (file)
index 0000000..234095c
--- /dev/null
@@ -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.local.dto;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * The {@link LocalScheduleResponse} class models the LocalSchedule response message
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class LocalScheduleResponse extends Response {
+
+    public List<Running> running = new LinkedList<Running>();
+
+    public List<Relay> relays = new LinkedList<Relay>();
+
+    public String name;
+
+    public Integer time;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/PlanArray.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/PlanArray.java
new file mode 100644 (file)
index 0000000..063bcdc
--- /dev/null
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.api.local.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link PlanArray} class models am account plan.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class PlanArray {
+
+    public String id;
+
+    public Object sku;
+
+    public String discount;
+
+    public String cost;
+
+    public String costUs;
+
+    public String costAu;
+
+    public String costEu;
+
+    public String costCa;
+
+    public String costUk;
+
+    public String active;
+
+    public String controllerQty;
+
+    public String rainfall;
+
+    public String smsQty;
+
+    public String scheduledReports;
+
+    public String emailAlerts;
+
+    public String defineSensor;
+
+    public String addUser;
+
+    public String contractor;
+
+    public String description;
+
+    public String sensorPack;
+
+    public String filelimit;
+
+    public String filetypeall;
+
+    @SerializedName(value = "plan_type")
+    public String planType;
+
+    public String pushNotification;
+
+    public String weatherQty;
+
+    public String weatherFreeQty;
+
+    public String reportingDays;
+
+    public String weatherHourlyUpdates;
+
+    public String freeEnthusiastPlans;
+
+    public String visible;
+
+    public String contractorPurchasable;
+
+    public String boc;
+
+    public String expiry;
+
+    public String start;
+
+    public String customerplanId;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Relay.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Relay.java
new file mode 100644 (file)
index 0000000..bbf9521
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.api.local.dto;
+
+/**
+ * The {@link Relay} class models the Relay response message
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Relay {
+
+    public Integer relayId;
+
+    public Integer time;
+
+    public Integer type;
+
+    public Integer relay;
+
+    public String name;
+
+    public Integer frequency;
+
+    public String timestr;
+
+    public Integer runSeconds;
+
+    /**
+     * Returns back the actual relay number when multiple controllers are chained.
+     *
+     * @return
+     */
+    public int getRelayNumber() {
+        int quotient = relay / 100;
+        return (relay - (quotient * 100)) + (quotient * 12);
+    }
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Response.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Response.java
new file mode 100644 (file)
index 0000000..329c244
--- /dev/null
@@ -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.local.dto;
+
+/**
+ * The {@link Response} class models Response messages
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Response {
+
+    public String errorMsg;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Running.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Running.java
new file mode 100644 (file)
index 0000000..f7bfe5f
--- /dev/null
@@ -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.local.dto;
+
+/**
+ * The {@link Running} class models a running relay
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Running {
+
+    public String relay;
+
+    public String relayId;
+
+    public Integer timeLeft;
+
+    public String run;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Sensor.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Sensor.java
new file mode 100644 (file)
index 0000000..de95c08
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.api.local.dto;
+
+import java.util.List;
+
+/**
+ * The {@link Sensor} class models a sensor
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class Sensor {
+
+    public Integer input;
+
+    public Integer type;
+
+    public Integer mode;
+
+    public Integer timer;
+
+    public Integer offtimer;
+
+    public String name;
+
+    public Integer offlevel;
+
+    public Integer active;
+
+    public List<Object> relays = null;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/SetControllerResponse.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/SetControllerResponse.java
new file mode 100644 (file)
index 0000000..2f924d2
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.api.local.dto;
+
+/**
+ * The {@link SetControllerResponse} class models the SetController response message
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class SetControllerResponse extends Response {
+
+    public String name;
+
+    public String controllerId;
+
+    public String message;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/SetZoneResponse.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/SetZoneResponse.java
new file mode 100644 (file)
index 0000000..44768ee
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.api.local.dto;
+
+/**
+ * The {@link SetZoneResponse} class models the SetZone response message
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class SetZoneResponse extends Response {
+
+    public String message;
+
+    public String messageType;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/StatusScheduleResponse.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/StatusScheduleResponse.java
new file mode 100644 (file)
index 0000000..e506f8b
--- /dev/null
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.api.local.dto;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * The {@link StatusScheduleResponse} class models the Status and Schedule response message
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class StatusScheduleResponse extends LocalScheduleResponse {
+
+    public Integer controllerId;
+
+    public Integer customerId;
+
+    public Integer userId;
+
+    public Integer nextpoll;
+
+    public List<Sensor> sensors = new LinkedList<Sensor>();
+
+    public String message;
+
+    public String obsRain;
+
+    public String obsRainWeek;
+
+    public String obsMaxtemp;
+
+    public Integer obsRainUpgrade;
+
+    public String obsRainText;
+
+    public String obsCurrenttemp;
+
+    public String wateringTime;
+
+    public Integer waterSaving;
+
+    public String lastContact;
+
+    public List<Forecast> forecast = new LinkedList<Forecast>();
+
+    public String status;
+
+    public String statusIcon;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/BocTopologyActual.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/BocTopologyActual.java
deleted file mode 100644 (file)
index 06098af..0000000
+++ /dev/null
@@ -1,25 +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.model;
-
-import java.util.List;
-
-/**
- * The {@link BocTopologyDesired} class models the actual BocTopology
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class BocTopologyActual {
-
-    public List<Object> bocGateways;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/BocTopologyDesired.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/BocTopologyDesired.java
deleted file mode 100644 (file)
index 795594c..0000000
+++ /dev/null
@@ -1,25 +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.model;
-
-import java.util.List;
-
-/**
- * The {@link BocTopologyDesired} class models the desired BocTopology
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class BocTopologyDesired {
-
-    public List<Object> bocGateways;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Controller.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Controller.java
deleted file mode 100644 (file)
index 2658fa9..0000000
+++ /dev/null
@@ -1,65 +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.model;
-
-import java.util.List;
-
-/**
- * The {@link Controller} class models a Hydrawise controller unit
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class Controller {
-
-    public String name;
-
-    public Integer lastContact;
-
-    public String serialNumber;
-
-    public Integer controllerId;
-
-    public String swVersion;
-
-    public String hardware;
-
-    public Boolean isBoc;
-
-    public String address;
-
-    public String timezone;
-
-    public Integer deviceId;
-
-    public Object parentDeviceId;
-
-    public String image;
-
-    public String description;
-
-    public Integer customerId;
-
-    public Double latitude;
-
-    public Double longitude;
-
-    public String lastContactReadable;
-
-    public String status;
-
-    public String statusIcon;
-
-    public Boolean online;
-
-    public List<String> tags = null;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/CustomerDetailsResponse.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/CustomerDetailsResponse.java
deleted file mode 100644 (file)
index 71db2a3..0000000
+++ /dev/null
@@ -1,49 +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.model;
-
-import java.util.List;
-
-/**
- * The {@link CustomerDetailsResponse} class models the CustomerDetails response message
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class CustomerDetailsResponse extends Response {
-
-    public BocTopologyDesired bocTopologyDesired;
-
-    public BocTopologyActual bocTopologyActual;
-
-    public List<Controller> controllers;
-
-    public String currentController;
-
-    public Boolean isBoc;
-
-    public Integer tandc;
-
-    public Integer controllerId;
-
-    public Integer customerId;
-
-    public String sessionId;
-
-    public String hardwareVersion;
-
-    public Integer deviceId;
-
-    public Integer tandcVersion;
-
-    public Features features;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Features.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Features.java
deleted file mode 100644 (file)
index cf89a8c..0000000
+++ /dev/null
@@ -1,101 +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.model;
-
-import java.util.List;
-
-/**
- * The {@link Features} class models an accounts features.
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class Features {
-
-    public List<PlanArray> planArray = null;
-
-    public Object id;
-
-    public String planType2;
-
-    public String planType2Key;
-
-    public Object sku;
-
-    public String discount;
-
-    public String cost;
-
-    public String costUs;
-
-    public String costAu;
-
-    public String costEu;
-
-    public String costCa;
-
-    public String costUk;
-
-    public String active;
-
-    public String controllerQty;
-
-    public String rainfall;
-
-    public String smsQty;
-
-    public String scheduledReports;
-
-    public String emailAlerts;
-
-    public String defineSensor;
-
-    public String addUser;
-
-    public String contractor;
-
-    public Object description;
-
-    public String sensorPack;
-
-    public String filelimit;
-
-    public String filetypeall;
-
-    public String planType;
-
-    public String pushNotification;
-
-    public String weatherQty;
-
-    public String weatherFreeQty;
-
-    public String reportingDays;
-
-    public String weatherHourlyUpdates;
-
-    public String freeEnthusiastPlans;
-
-    public String visible;
-
-    public Object contractorPurchasable;
-
-    public Integer boc;
-
-    public Object expiry;
-
-    public Object start;
-
-    public String customerplanId;
-
-    public Integer smsUsed;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Forecast.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Forecast.java
deleted file mode 100644 (file)
index ee1034a..0000000
+++ /dev/null
@@ -1,39 +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.model;
-
-/**
- * The {@link Forecast} class models a daily weather forecast
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class Forecast {
-
-    public String tempHi;
-
-    public String tempLo;
-
-    public String conditions;
-
-    public String day;
-
-    public Integer pop;
-
-    public Integer humidity;
-
-    public String wind;
-
-    public String icon;
-
-    public String iconLocal;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/LocalScheduleResponse.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/LocalScheduleResponse.java
deleted file mode 100644 (file)
index 23dd6ab..0000000
+++ /dev/null
@@ -1,32 +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.model;
-
-import java.util.LinkedList;
-import java.util.List;
-
-/**
- * The {@link LocalScheduleResponse} class models the LocalSchedule response message
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class LocalScheduleResponse extends Response {
-
-    public List<Running> running = new LinkedList<>();
-
-    public List<Relay> relays = new LinkedList<>();
-
-    public String name;
-
-    public Integer time;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/PlanArray.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/PlanArray.java
deleted file mode 100644 (file)
index e6be96f..0000000
+++ /dev/null
@@ -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.api.model;
-
-/**
- * The {@link PlanArray} class models am account plan.
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class PlanArray {
-
-    public String id;
-
-    public Object sku;
-
-    public String discount;
-
-    public String cost;
-
-    public String costUs;
-
-    public String costAu;
-
-    public String costEu;
-
-    public String costCa;
-
-    public String costUk;
-
-    public String active;
-
-    public String controllerQty;
-
-    public String rainfall;
-
-    public String smsQty;
-
-    public String scheduledReports;
-
-    public String emailAlerts;
-
-    public String defineSensor;
-
-    public String addUser;
-
-    public String contractor;
-
-    public String description;
-
-    public String sensorPack;
-
-    public String filelimit;
-
-    public String filetypeall;
-
-    public String plan_type;
-
-    public String pushNotification;
-
-    public String weatherQty;
-
-    public String weatherFreeQty;
-
-    public String reportingDays;
-
-    public String weatherHourlyUpdates;
-
-    public String freeEnthusiastPlans;
-
-    public String visible;
-
-    public String contractorPurchasable;
-
-    public String boc;
-
-    public String expiry;
-
-    public String start;
-
-    public String customerplanId;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Relay.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Relay.java
deleted file mode 100644 (file)
index 3945b47..0000000
+++ /dev/null
@@ -1,57 +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.model;
-
-import com.google.gson.annotations.SerializedName;
-
-/**
- * The {@link Relay} class models the Relay response message
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class Relay {
-
-    public Integer relayId;
-
-    public Integer relay;
-
-    public String name;
-
-    public String icon;
-
-    public String lastwater;
-
-    public Integer time;
-
-    public Integer type;
-
-    @SerializedName("run")
-    public String runTime;
-
-    @SerializedName("run_seconds")
-    public Integer runTimeSeconds;
-
-    public String nicetime;
-
-    public String id;
-
-    /**
-     * Returns back the actual relay number when multiple controllers are chained.
-     *
-     * @return
-     */
-    public int getRelayNumber() {
-        int quotient = relay / 100;
-        return (relay - (quotient * 100)) + (quotient * 12);
-    }
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Response.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Response.java
deleted file mode 100644 (file)
index b6211fc..0000000
+++ /dev/null
@@ -1,23 +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.model;
-
-/**
- * The {@link Response} class models Response messages
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class Response {
-
-    public String errorMsg;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Running.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Running.java
deleted file mode 100644 (file)
index db218e3..0000000
+++ /dev/null
@@ -1,29 +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.model;
-
-/**
- * The {@link Running} class models a running relay
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class Running {
-
-    public String relay;
-
-    public String relayId;
-
-    public Integer timeLeft;
-
-    public String run;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Sensor.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Sensor.java
deleted file mode 100644 (file)
index bab38e9..0000000
+++ /dev/null
@@ -1,41 +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.model;
-
-import java.util.List;
-
-/**
- * The {@link Sensor} class models a sensor
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class Sensor {
-
-    public Integer input;
-
-    public Integer type;
-
-    public Integer mode;
-
-    public Integer timer;
-
-    public Integer offtimer;
-
-    public String name;
-
-    public Integer offlevel;
-
-    public Integer active;
-
-    public List<Object> relays = null;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/SetControllerResponse.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/SetControllerResponse.java
deleted file mode 100644 (file)
index a392f3e..0000000
+++ /dev/null
@@ -1,27 +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.model;
-
-/**
- * The {@link SetControllerResponse} class models the SetController response message
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class SetControllerResponse extends Response {
-
-    public String name;
-
-    public String controllerId;
-
-    public String message;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/SetZoneResponse.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/SetZoneResponse.java
deleted file mode 100644 (file)
index 223ca0a..0000000
+++ /dev/null
@@ -1,25 +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.model;
-
-/**
- * The {@link SetZoneResponse} class models the SetZone response message
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class SetZoneResponse extends Response {
-
-    public String message;
-
-    public String messageType;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/StatusScheduleResponse.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/StatusScheduleResponse.java
deleted file mode 100644 (file)
index 65adc2f..0000000
+++ /dev/null
@@ -1,60 +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.model;
-
-import java.util.LinkedList;
-import java.util.List;
-
-/**
- * The {@link StatusScheduleResponse} class models the Status and Schedule response message
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class StatusScheduleResponse extends LocalScheduleResponse {
-
-    public Integer controllerId;
-
-    public Integer customerId;
-
-    public Integer userId;
-
-    public Integer nextpoll;
-
-    public List<Sensor> sensors = new LinkedList<>();
-
-    public String message;
-
-    public String obsRain;
-
-    public String obsRainWeek;
-
-    public String obsMaxtemp;
-
-    public Integer obsRainUpgrade;
-
-    public String obsRainText;
-
-    public String obsCurrenttemp;
-
-    public String wateringTime;
-
-    public Integer waterSaving;
-
-    public String lastContact;
-
-    public List<Forecast> forecast = new LinkedList<>();
-
-    public String status;
-
-    public String statusIcon;
-}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/config/HydrawiseAccountConfiguration.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/config/HydrawiseAccountConfiguration.java
new file mode 100644 (file)
index 0000000..75c78d6
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/config/HydrawiseControllerConfiguration.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/config/HydrawiseControllerConfiguration.java
new file mode 100644 (file)
index 0000000..c88a689
--- /dev/null
@@ -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 HydrawiseControllerConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class HydrawiseControllerConfiguration {
+    /**
+     * optional id of the controller to connect to
+     */
+    public Integer controllerId = -1;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/config/HydrawiseLocalConfiguration.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/config/HydrawiseLocalConfiguration.java
new file mode 100644 (file)
index 0000000..38fdc02
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link HydrawiseLocalConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class HydrawiseLocalConfiguration {
+    /**
+     * Host or IP for local controller
+     */
+    public String host = "";
+    /**
+     * User name (admin) for local controller
+     */
+    public String username = "";
+    /**
+     * Password for local controller
+     */
+    public String password = "";
+    /**
+     * refresh interval in seconds.
+     */
+    public int refresh = 30;
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/discovery/HydrawiseCloudControllerDiscoveryService.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/discovery/HydrawiseCloudControllerDiscoveryService.java
new file mode 100644 (file)
index 0000000..f51354e
--- /dev/null
@@ -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());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseAccountHandler.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseAccountHandler.java
new file mode 100644 (file)
index 0000000..7def5c7
--- /dev/null
@@ -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();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseControllerHandler.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseControllerHandler.java
new file mode 100644 (file)
index 0000000..eb36de0
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseLocalHandler.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseLocalHandler.java
new file mode 100644 (file)
index 0000000..74248fe
--- /dev/null
@@ -0,0 +1,314 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hydrawise.internal.handler;
+
+import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
+
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
+import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
+import org.openhab.binding.hydrawise.internal.api.local.HydrawiseLocalApiClient;
+import org.openhab.binding.hydrawise.internal.api.local.dto.LocalScheduleResponse;
+import org.openhab.binding.hydrawise.internal.api.local.dto.Relay;
+import org.openhab.binding.hydrawise.internal.api.local.dto.Running;
+import org.openhab.binding.hydrawise.internal.config.HydrawiseLocalConfiguration;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link HydrawiseLocalHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class HydrawiseLocalHandler extends BaseThingHandler {
+    private final Logger logger = LoggerFactory.getLogger(HydrawiseLocalHandler.class);
+    protected final Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<>());
+    protected final Map<String, Relay> relayMap = Collections.synchronizedMap(new HashMap<>());
+    private @Nullable ScheduledFuture<?> pollFuture;
+
+    /**
+     * value observed being used by the Hydrawise clients as a max time value,
+     */
+    private static final long MAX_RUN_TIME = 157680000;
+
+    /**
+     * Minimum amount of time we can poll for updates
+     */
+    protected static final int MIN_REFRESH_SECONDS = 5;
+
+    /**
+     * Minimum amount of time we can poll after a command
+     */
+    protected static final int COMMAND_REFRESH_SECONDS = 5;
+
+    /**
+     * Our poll rate
+     */
+    protected int refresh;
+
+    /**
+     * Future to poll for updated
+     */
+
+    HydrawiseLocalApiClient client;
+
+    public HydrawiseLocalHandler(Thing thing, HttpClient httpClient) {
+        super(thing);
+        client = new HydrawiseLocalApiClient(httpClient);
+    }
+
+    @Override
+    public void initialize() {
+        scheduler.schedule(this::configureInternal, 0, TimeUnit.SECONDS);
+    }
+
+    @Override
+    public void dispose() {
+        logger.debug("Handler disposed.");
+        clearPolling();
+    }
+
+    @Override
+    public void channelLinked(ChannelUID channelUID) {
+        // clear our cached value so the new channel gets updated on the next poll
+        stateMap.remove(channelUID.getId());
+    }
+
+    @SuppressWarnings({ "null", "unused" }) // compiler does not like relayMap.get can return null
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        if (getThing().getStatus() != ThingStatus.ONLINE) {
+            logger.warn("Controller is NOT ONLINE and is not responding to commands");
+            return;
+        }
+
+        // remove our cached state for this, will be safely updated on next poll
+        stateMap.remove(channelUID.getAsString());
+
+        if (command instanceof RefreshType) {
+            // we already removed this from the cache
+            return;
+        }
+
+        String group = channelUID.getGroupId();
+        String channelId = channelUID.getIdWithoutGroup();
+        boolean allCommand = CHANNEL_GROUP_ALLZONES.equals(group);
+
+        Relay relay = relayMap.get(group);
+        if (!allCommand && relay == null) {
+            logger.debug("Zone not found {}", group);
+            return;
+        }
+
+        try {
+            clearPolling();
+            switch (channelId) {
+                case CHANNEL_ZONE_RUN_CUSTOM:
+                    if (!(command instanceof QuantityType<?>)) {
+                        logger.warn("Invalid command type for run custom {}", command.getClass().getName());
+                        return;
+                    }
+                    if (allCommand) {
+                        client.runAllRelays(((QuantityType<?>) command).intValue());
+                    } else {
+                        client.runRelay(((QuantityType<?>) command).intValue(), relay.relay);
+                    }
+                    break;
+                case CHANNEL_ZONE_RUN:
+                    if (!(command instanceof OnOffType)) {
+                        logger.warn("Invalid command type for run {}", command.getClass().getName());
+                        return;
+                    }
+                    if (allCommand) {
+                        if (command == OnOffType.ON) {
+                            client.runAllRelays();
+                        } else {
+                            client.stopAllRelays();
+                        }
+                    } else {
+                        if (command == OnOffType.ON) {
+                            client.runRelay(relay.relay);
+                        } else {
+                            client.stopRelay(relay.relay);
+                        }
+                    }
+                    break;
+            }
+            initPolling(COMMAND_REFRESH_SECONDS);
+        } catch (HydrawiseCommandException | HydrawiseConnectionException e) {
+            logger.debug("Could not issue command", e);
+            initPolling(COMMAND_REFRESH_SECONDS);
+        } catch (HydrawiseAuthenticationException e) {
+            logger.debug("Credentials not valid");
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
+            configureInternal();
+        }
+    }
+
+    protected void updateZones(LocalScheduleResponse status) {
+        ZonedDateTime now = ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+        status.relays.forEach(r -> {
+            String group = "zone" + r.getRelayNumber();
+            relayMap.put(group, r);
+            logger.trace("Updateing Zone {} {} ", group, r.name);
+            updateGroupState(group, CHANNEL_ZONE_NAME, new StringType(r.name));
+            updateGroupState(group, CHANNEL_ZONE_TYPE, new DecimalType(r.type));
+            updateGroupState(group, CHANNEL_ZONE_STARTTIME,
+                    r.runSeconds != null ? new QuantityType<>(r.runSeconds, Units.SECOND) : UnDefType.UNDEF);
+            if (r.time >= MAX_RUN_TIME) {
+                updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME, UnDefType.UNDEF);
+            } else {
+                updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME,
+                        new DateTimeType(now.plusSeconds(r.time).truncatedTo(ChronoUnit.MINUTES)));
+            }
+
+            Optional<Running> running = status.running.stream()
+                    .filter(z -> Integer.parseInt(z.relayId) == r.relayId.intValue()).findAny();
+            if (running.isPresent()) {
+                updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.ON);
+                updateGroupState(group, CHANNEL_ZONE_TIME_LEFT,
+                        new QuantityType<>(running.get().timeLeft, Units.SECOND));
+                logger.debug("{} Time Left {}", r.name, running.get().timeLeft);
+
+            } else {
+                updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.OFF);
+                updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new QuantityType<>(0, Units.SECOND));
+            }
+
+            updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_RUN,
+                    status.running.size() > 0 ? OnOffType.ON : OnOffType.OFF);
+        });
+    }
+
+    @SuppressWarnings("serial")
+    protected class NotConfiguredException extends Exception {
+        NotConfiguredException(String message) {
+            super(message);
+        }
+    }
+
+    private boolean isFutureValid(@Nullable ScheduledFuture<?> future) {
+        return future != null && !future.isCancelled();
+    }
+
+    private void configureInternal() {
+        clearPolling();
+        stateMap.clear();
+        relayMap.clear();
+        try {
+            HydrawiseLocalConfiguration configuration = getConfig().as(HydrawiseLocalConfiguration.class);
+            this.refresh = Math.max(configuration.refresh, MIN_REFRESH_SECONDS);
+            logger.trace("Connecting to host {}", configuration.host);
+            client.setCredentials(configuration.host, configuration.username, configuration.password);
+            LocalScheduleResponse response = client.getLocalSchedule();
+            if (response != null) {
+                updateZones(response);
+                initPolling(refresh);
+            } else {
+                logger.debug("Could not connect to service");
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                        "Invalid response from service");
+            }
+        } catch (HydrawiseConnectionException e) {
+            logger.debug("Could not connect to service");
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+        } catch (HydrawiseAuthenticationException e) {
+            logger.debug("Credentials not valid");
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
+        }
+    }
+
+    /**
+     * Starts/Restarts polling with an initial delay. This allows changes in the poll cycle for when commands are sent
+     * and we need to poll sooner then the next refresh cycle.
+     */
+    private synchronized void initPolling(int initalDelay) {
+        clearPolling();
+        pollFuture = scheduler.scheduleWithFixedDelay(this::pollControllerInternal, initalDelay, refresh,
+                TimeUnit.SECONDS);
+    }
+
+    /**
+     * Stops/clears this thing's polling future
+     */
+    private void clearPolling() {
+        ScheduledFuture<?> localFuture = pollFuture;
+        if (isFutureValid(localFuture)) {
+            if (localFuture != null) {
+                localFuture.cancel(false);
+            }
+        }
+    }
+
+    /**
+     * Poll the controller for updates.
+     */
+    private void pollControllerInternal() {
+        try {
+            LocalScheduleResponse response = client.getLocalSchedule();
+            if (response != null) {
+                updateZones(response);
+            }
+            if (getThing().getStatus() != ThingStatus.ONLINE) {
+                updateStatus(ThingStatus.ONLINE);
+            }
+        } catch (HydrawiseConnectionException e) {
+            // poller will continue to run, set offline until next run
+            logger.debug("Exception polling", e);
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+        } catch (HydrawiseAuthenticationException e) {
+            // if are creds are not valid, we need to try re authorizing again
+            logger.debug("Authorization exception during polling", e);
+            configureInternal();
+        }
+    }
+
+    private void updateGroupState(String group, String channelID, State state) {
+        String channelName = group + "#" + channelID;
+        State oldState = stateMap.put(channelName, state);
+        if (!state.equals(oldState)) {
+            ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelName);
+            logger.debug("updateState updating {} {}", channelUID, state);
+            updateState(channelUID, state);
+        }
+    }
+}
index a7532abfbe901466221c9ecb81fd5054d566bcd3..f6f7fddc866ff704b875f90fd8814cab8d2e03fa 100644 (file)
                <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>
 
                <channels>
                        <channel id="name" typeId="name"/>
                        <channel id="input" typeId="input"/>
-                       <channel id="mode" typeId="mode"/>
-                       <channel id="timer" typeId="timer"/>
+                       <channel id="delay" typeId="delay"/>
                        <channel id="offtimer" typeId="offtimer"/>
                        <channel id="offlevel" typeId="offlevel"/>
                        <channel id="active" typeId="active"/>
+                       <channel id="waterflow" typeId="waterflow"/>
                </channels>
        </channel-group-type>
 
                        <channel id="temperaturehigh" typeId="temperaturehigh"/>
                        <channel id="temperaturelow" typeId="temperaturelow"/>
                        <channel id="conditions" typeId="conditions"/>
-                       <channel id="day" typeId="day"/>
+                       <channel id="time" typeId="time"/>
                        <channel id="humidity" typeId="humidity"/>
                        <channel id="wind" typeId="wind"/>
+                       <channel id="evapotranspiration" typeId="evapotranspiration"/>
+                       <channel id="precipitation" typeId="precipitation"/>
+                       <channel id="probabilityofprecipitation" typeId="probabilityofprecipitation"/>
+               </channels>
+       </channel-group-type>
+
+       <channel-group-type id="system">
+               <label>System</label>
+               <description>Controller system data</description>
+               <channels>
+                       <channel id="name" typeId="name"/>
+                       <channel id="summary" typeId="summary"/>
+                       <channel id="lastcontacttime" typeId="lastcontacttime"/>
                </channels>
        </channel-group-type>
 
        <!-- Controller -->
+
+       <channel-type id="lastcontacttime" advanced="true">
+               <item-type>DateTime</item-type>
+               <label>Last Contact Time</label>
+               <description>Last contact time of a controller</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="summary">
+               <item-type>String</item-type>
+               <label>Status Summary</label>
+               <description>Status summary</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <!-- Zones -->
+
        <channel-type id="name">
                <item-type>String</item-type>
                <label>Name</label>
                <state readOnly="true"></state>
        </channel-type>
 
-       <channel-type id="time" advanced="true">
-               <item-type>Number</item-type>
+       <channel-type id="starttime" advanced="true">
+               <item-type>DateTime</item-type>
                <label>Start Time</label>
-               <description>Zone start time in seconds</description>
+               <description>Next zone start time</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="duration" advanced="true">
+               <item-type>Number:Time</item-type>
+               <label>Duration</label>
+               <description>Next start duration</description>
                <state readOnly="true"></state>
        </channel-type>
 
        <channel-type id="nextruntime">
                <item-type>DateTime</item-type>
                <label>Next Run Time</label>
-               <description>Next time this zone is scheduled to run</description>
+               <description>The next time this zone is scheduled to run</description>
                <state readOnly="true"></state>
        </channel-type>
 
        </channel-type>
 
        <channel-type id="runcustom">
-               <item-type>Number</item-type>
-               <label>Run Zones With Custom Duration </label>
-               <description>Run zones now for a custom duration of time in seconds</description>
+               <item-type>Number:Time</item-type>
+               <label>Run Zones With Custom Duration</label>
+               <description>Run zones now for a custom duration</description>
+       </channel-type>
+
+       <channel-type id="suspend">
+               <item-type>Switch</item-type>
+               <label>Suspend Zones</label>
+               <description>Suspends or resumes zones</description>
+       </channel-type>
+
+       <channel-type id="suspenduntil">
+               <item-type>DateTime</item-type>
+               <label>Suspend Zones</label>
+               <description>Suspends zones until this date</description>
        </channel-type>
 
        <channel-type id="timeleft">
-               <item-type>Number</item-type>
+               <item-type>Number:Time</item-type>
                <label>Time Left Seconds</label>
                <description>Time left that zone will run for</description>
                <state readOnly="true"></state>
                <state readOnly="true"></state>
        </channel-type>
 
-       <channel-type id="mode" advanced="true">
-               <item-type>Number</item-type>
-               <label>Mode</label>
-               <description>Sensor mode</description>
-               <state readOnly="true"></state>
-       </channel-type>
-
-       <channel-type id="timer" advanced="true">
-               <item-type>Number</item-type>
-               <label>Timer</label>
-               <description>Sensor timer</description>
+       <channel-type id="delay" advanced="true">
+               <item-type>Number:Time</item-type>
+               <label>Delay</label>
+               <description>Sensor delay</description>
                <state readOnly="true"></state>
        </channel-type>
 
        <channel-type id="offtimer" advanced="true">
-               <item-type>Number</item-type>
+               <item-type>Number:Time</item-type>
                <label>Off Timer</label>
                <description>Sensor off timer</description>
                <state readOnly="true"></state>
                <description>Sensor off level</description>
                <state readOnly="true"></state>
        </channel-type>
+
        <channel-type id="active">
                <item-type>Switch</item-type>
                <label>Active</label>
                <state readOnly="true"></state>
        </channel-type>
 
+       <channel-type id="waterflow" advanced="true">
+               <item-type>Number:Volume</item-type>
+               <label>Water Flow</label>
+               <description>Sensor water flow</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
        <!-- Weather Forecast -->
        <channel-type id="temperaturehigh">
                <item-type>Number:Temperature</item-type>
                <state readOnly="true"></state>
        </channel-type>
 
-       <channel-type id="day">
-               <item-type>String</item-type>
-               <label>Day of Week</label>
-               <description>Day of week for the weather forecast</description>
+       <channel-type id="time">
+               <item-type>DateTime</item-type>
+               <label>Forecast Time</label>
+               <description>Forecast date and time</description>
                <state readOnly="true"></state>
        </channel-type>
 
                <category>Wind</category>
                <state readOnly="true" pattern="%.1f %unit%"/>
        </channel-type>
+
+       <channel-type id="evapotranspiration">
+               <item-type>Number</item-type>
+               <label>Evapotranspiration</label>
+               <description>Evapotranspiration amount</description>
+               <state readOnly="true" pattern="%.1f"/>
+       </channel-type>
+
+       <channel-type id="precipitation">
+               <item-type>Number</item-type>
+               <label>Precipitation</label>
+               <description>Precipitation amount</description>
+               <state readOnly="true" pattern="%.1f"/>
+       </channel-type>
+
+       <channel-type id="probabilityofprecipitation">
+               <item-type>Number</item-type>
+               <label>Probability Of Precipitation</label>
+               <description>Probability of precipitation percentage</description>
+               <state readOnly="true" pattern="%d%%"/>
+       </channel-type>
 </thing:thing-descriptions>
index b783b67833a8694a00133470c3abff1fcf4b06b8..8f0434e97e31fb7518263dbcdce8534ac46d1530 100644 (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>
                                <label>Day 3 Weather</label>
                                <description>Day 3 weather forecast</description>
                        </channel-group>
-                       <channel-group id="forecast4" typeId="forecast">
-                               <label>Day 4 Weather</label>
-                               <description>Day 4 weather forecast</description>
-                       </channel-group>
+
+                       <!-- All Zones -->
 
                        <channel-group id="allzones" typeId="allzones"/>
 
+                       <!-- Zones -->
+
                        <channel-group id="zone1" typeId="zone">
                                <label>Zone 1</label>
                                <description>Sprinkler Zone 1</description>
                        </channel-group>
                </channel-groups>
                <config-description>
-                       <parameter name="apiKey" type="text" required="true">
-                               <label>API Key</label>
-                               <description>API Key from https://app.hydrawise.com/config/account</description>
-                       </parameter>
-                       <parameter name="refresh" type="integer" required="true">
-                               <label>Refresh interval</label>
-                               <description>Specifies the refresh interval in seconds</description>
-                               <default>30</default>
-                       </parameter>
-                       <parameter name="controllerId" type="integer" required="false">
-                               <label>Optional Controller ID interval</label>
-                               <description>Optional parameter to specify the Hydrawise controller ID if you have more then one associated with
-                                       your account.
+                       <parameter name="controllerId" type="integer" required="true">
+                               <label>Controller ID</label>
+                               <description>The ID of a cloud connected irrigation controller
                                </description>
                        </parameter>
                </config-description>
 
        <thing-type id="local">
                <label>Hydrawise Local Thing</label>
-               <description>Hydrawise local connected irrigation system</description>
+               <description>Hydrawise local connected irrigation controller</description>
                <channel-groups>
                        <channel-group id="zone1" typeId="zone">
                                <label>Zone 1</label>
                        </parameter>
                </config-description>
        </thing-type>
-
-
 </thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.hydrawise/src/main/resources/query.graphql b/bundles/org.openhab.binding.hydrawise/src/main/resources/query.graphql
new file mode 100644 (file)
index 0000000..6699a94
--- /dev/null
@@ -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
+        }
+      }
+    }
+  }
+}