From 97144acdae68d9236bde3a6002f006b5142f0e6d Mon Sep 17 00:00:00 2001 From: Dan Cunningham Date: Sun, 1 Aug 2021 11:03:37 -0700 Subject: [PATCH] [hydrawise] Migrate to new GraphQL based API (#10947) * [hydrawise] Migrated to new GraphQL based API Fixes #7261 Signed-off-by: Dan Cunningham * Addressed PR comments. Signed-off-by: Dan Cunningham * Address PR review comments. Signed-off-by: Dan Cunningham Signed-off-by: Dave J Schoepel --- .../org.openhab.binding.hydrawise/README.md | 171 ++++--- .../internal/HydrawiseBindingConstants.java | 41 +- .../internal/HydrawiseCloudHandler.java | 243 ---------- .../internal/HydrawiseControllerListener.java | 28 ++ .../internal/HydrawiseHandlerFactory.java | 28 +- .../internal/HydrawiseLocalHandler.java | 91 ---- .../api/HydrawiseAuthenticationException.java | 17 +- .../internal/api/HydrawiseCloudApiClient.java | 312 ------------- .../api/HydrawiseCommandException.java | 6 +- .../api/HydrawiseConnectionException.java | 5 +- .../api/graphql/HydrawiseGraphQLClient.java | 341 ++++++++++++++ .../internal/api/graphql/dto/AuthToken.java | 29 ++ .../internal/api/graphql/dto/Controller.java | 29 ++ .../api/graphql/dto/ControllerStatus.java | 26 ++ .../internal/api/graphql/dto/Coordinates.java | 21 + .../internal/api/graphql/dto/Customer.java | 24 + .../internal/api/graphql/dto/Data.java | 20 + .../internal/api/graphql/dto/Forecast.java | 29 ++ .../internal/api/graphql/dto/Icon.java | 22 + .../internal/api/graphql/dto/Input.java | 21 + .../internal/api/graphql/dto/Location.java | 23 + .../internal/api/graphql/dto/Mutation.java | 26 ++ .../api/graphql/dto/MutationResponse.java | 32 ++ .../internal/api/graphql/dto/PastRuns.java | 20 + .../api/graphql/dto/QueryRequest.java | 24 + .../api/graphql/dto/QueryResponse.java | 23 + .../api/graphql/dto/QueryResponseError.java | 21 + .../dto/QueryResponseErrorExtensions.java | 20 + .../api/graphql/dto/ScheduledRuns.java | 22 + .../internal/api/graphql/dto/Sensor.java | 24 + .../internal/api/graphql/dto/SensorModel.java | 24 + .../api/graphql/dto/SensorStatus.java | 21 + .../internal/api/graphql/dto/Time.java | 20 + .../internal/api/graphql/dto/UnitValue.java | 21 + .../internal/api/graphql/dto/Zone.java | 26 ++ .../internal/api/graphql/dto/ZoneNumber.java | 21 + .../internal/api/graphql/dto/ZoneRun.java | 23 + .../internal/api/graphql/dto/ZoneStatus.java | 20 + .../{ => local}/HydrawiseLocalApiClient.java | 14 +- .../HydrawiseZoneCommandBuilder.java | 5 +- .../dto}/BocTopologyActual.java | 2 +- .../dto}/BocTopologyDesired.java | 2 +- .../api/{model => local/dto}/Controller.java | 4 +- .../dto}/CustomerDetailsResponse.java | 2 +- .../api/{model => local/dto}/Features.java | 2 +- .../api/{model => local/dto}/Forecast.java | 2 +- .../dto}/LocalScheduleResponse.java | 6 +- .../api/{model => local/dto}/PlanArray.java | 7 +- .../api/{model => local/dto}/Relay.java | 24 +- .../api/{model => local/dto}/Response.java | 2 +- .../api/{model => local/dto}/Running.java | 2 +- .../api/{model => local/dto}/Sensor.java | 2 +- .../dto}/SetControllerResponse.java | 2 +- .../{model => local/dto}/SetZoneResponse.java | 2 +- .../dto}/StatusScheduleResponse.java | 6 +- .../config/HydrawiseAccountConfiguration.java | 28 ++ .../HydrawiseControllerConfiguration.java} | 22 +- .../HydrawiseLocalConfiguration.java | 15 +- ...rawiseCloudControllerDiscoveryService.java | 110 +++++ .../handler/HydrawiseAccountHandler.java | 216 +++++++++ .../handler/HydrawiseControllerHandler.java | 436 ++++++++++++++++++ .../HydrawiseLocalHandler.java} | 165 +++---- .../resources/OH-INF/thing/channel-types.xml | 133 ++++-- .../main/resources/OH-INF/thing/things.xml | 74 ++- .../src/main/resources/query.graphql | 116 +++++ 65 files changed, 2369 insertions(+), 947 deletions(-) delete mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseCloudHandler.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseControllerListener.java delete mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseLocalHandler.java delete mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseCloudApiClient.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/HydrawiseGraphQLClient.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/AuthToken.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Controller.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ControllerStatus.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Coordinates.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Customer.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Data.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Forecast.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Icon.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Input.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Location.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Mutation.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/MutationResponse.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/PastRuns.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryRequest.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponse.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponseError.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponseErrorExtensions.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ScheduledRuns.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Sensor.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/SensorModel.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/SensorStatus.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Time.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/UnitValue.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Zone.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneNumber.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneRun.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneStatus.java rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{ => local}/HydrawiseLocalApiClient.java (93%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{ => local}/HydrawiseZoneCommandBuilder.java (95%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/BocTopologyActual.java (90%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/BocTopologyDesired.java (90%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/Controller.java (93%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/CustomerDetailsResponse.java (94%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/Features.java (96%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/Forecast.java (92%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/LocalScheduleResponse.java (79%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/PlanArray.java (89%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/Relay.java (73%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/Response.java (89%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/Running.java (91%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/Sensor.java (92%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/SetControllerResponse.java (91%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/SetZoneResponse.java (90%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/{model => local/dto}/StatusScheduleResponse.java (86%) create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/config/HydrawiseAccountConfiguration.java rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/{HydrawiseCloudConfiguration.java => config/HydrawiseControllerConfiguration.java} (54%) rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/{ => config}/HydrawiseLocalConfiguration.java (76%) create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/discovery/HydrawiseCloudControllerDiscoveryService.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseAccountHandler.java create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseControllerHandler.java rename bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/{HydrawiseHandler.java => handler/HydrawiseLocalHandler.java} (73%) create mode 100644 bundles/org.openhab.binding.hydrawise/src/main/resources/query.graphql diff --git a/bundles/org.openhab.binding.hydrawise/README.md b/bundles/org.openhab.binding.hydrawise/README.md index dfa54679cf444..33df7cb438367 100644 --- a/bundles/org.openhab.binding.hydrawise/README.md +++ b/bundles/org.openhab.binding.hydrawise/README.md @@ -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"} ``` diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseBindingConstants.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseBindingConstants.java index d578d4290ddd6..86071a804dc8a 100644 --- a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseBindingConstants.java +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseBindingConstants.java @@ -23,44 +23,57 @@ */ @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/HydrawiseCloudHandler.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseCloudHandler.java deleted file mode 100644 index fccc6a21997a3..0000000000000 --- a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseCloudHandler.java +++ /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 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 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 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 index 0000000000000..a546409e7c78e --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseControllerListener.java @@ -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 controllers); +} diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseHandlerFactory.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseHandlerFactory.java index 6ab1265676788..5033f45e1ef75 100644 --- a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseHandlerFactory.java +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseHandlerFactory.java @@ -15,13 +15,16 @@ 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 @@ @NonNullByDefault @Component(configurationPid = "binding.hydrawise", service = ThingHandlerFactory.class) public class HydrawiseHandlerFactory extends BaseThingHandlerFactory { - - private static final Set SUPPORTED_THING_TYPES_UIDS = Stream.of(THING_TYPE_CLOUD, THING_TYPE_LOCAL) - .collect(Collectors.toSet()); - - private final HttpClient httpClient; + private static final Set 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 boolean supportsThingType(ThingTypeUID thingTypeUID) { 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/HydrawiseLocalHandler.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseLocalHandler.java deleted file mode 100644 index 47525632780c6..0000000000000 --- a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseLocalHandler.java +++ /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(); - } -} diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseAuthenticationException.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseAuthenticationException.java index 2b6a42de6ef43..c4ff72573a65b 100644 --- a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseAuthenticationException.java +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseAuthenticationException.java @@ -12,12 +12,23 @@ */ package org.openhab.binding.hydrawise.internal.api; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** - * Thrown when the Hydrawise cloud or local API returns back a "unauthorized" response to commands - * + * Thrown when the Hydrawise API returns back a "unauthorized" response to commands + * * @author Dan Cunningham - Initial contribution */ -@SuppressWarnings("serial") +@NonNullByDefault public class HydrawiseAuthenticationException extends Exception { + private static final long serialVersionUID = 1L; + + public HydrawiseAuthenticationException() { + super(); + } + public HydrawiseAuthenticationException(@Nullable String message) { + super(message); + } } 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 index dad4e42cc7a5e..0000000000000 --- a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseCloudApiClient.java +++ /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); - } - } - } -} diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseCommandException.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseCommandException.java index d2d74b59013cf..742ae33efacd3 100644 --- a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseCommandException.java +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseCommandException.java @@ -12,13 +12,17 @@ */ package org.openhab.binding.hydrawise.internal.api; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Thrown when command responses return a error message * * @author Dan Cunningham - Initial contribution */ -@SuppressWarnings("serial") +@NonNullByDefault public class HydrawiseCommandException extends Exception { + private static final long serialVersionUID = 1L; + public HydrawiseCommandException(String message) { super(message); } diff --git a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseConnectionException.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseConnectionException.java index b4835d1cd040e..cc221425a94eb 100644 --- a/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseConnectionException.java +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseConnectionException.java @@ -12,13 +12,16 @@ */ package org.openhab.binding.hydrawise.internal.api; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Thrown for connection issues to the Hydrawise controller * * @author Dan Cunningham - Initial contribution */ -@SuppressWarnings("serial") +@NonNullByDefault public class HydrawiseConnectionException extends Exception { + private static final long serialVersionUID = 1L; public HydrawiseConnectionException(Exception e) { super(e); 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 index 0000000000000..9cd80fcf0a51a --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/HydrawiseGraphQLClient.java @@ -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()) + .registerTypeAdapter(ScheduledRuns.class, new ResponseDeserializer()) + .registerTypeAdapter(ZoneRun.class, new ResponseDeserializer()) + .registerTypeAdapter(Forecast.class, new ResponseDeserializer()) + .registerTypeAdapter(Sensor.class, new ResponseDeserializer()) + .registerTypeAdapter(ControllerStatus.class, new ResponseDeserializer()).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 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 implements JsonDeserializer { + @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 index 0000000000000..029df2985401e --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/AuthToken.java @@ -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 index 0000000000000..02d260a4215a8 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Controller.java @@ -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 zones = null; + public List sensors = null; + public List 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 index 0000000000000..f9b15b503ea78 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ControllerStatus.java @@ -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 index 0000000000000..0582966451cec --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Coordinates.java @@ -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 index 0000000000000..ca7ce639bb177 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Customer.java @@ -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 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 index 0000000000000..941255b0d758f --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Data.java @@ -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 index 0000000000000..1b3803bc9e83c --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Forecast.java @@ -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 index 0000000000000..2853613a1fa81 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Icon.java @@ -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 index 0000000000000..aaf9df74b6761 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Input.java @@ -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 index 0000000000000..c3023ecb3aaaa --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Location.java @@ -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; +} 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 index 0000000000000..d518a80da02a5 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Mutation.java @@ -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 index 0000000000000..b7908c1d12e85 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/MutationResponse.java @@ -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 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 index 0000000000000..dea3bf5ba4fb3 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/PastRuns.java @@ -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 index 0000000000000..5b2d4a0c6a08f --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryRequest.java @@ -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 index 0000000000000..49ad458705063 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponse.java @@ -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 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 index 0000000000000..771ac427b8591 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponseError.java @@ -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 index 0000000000000..fbf6c21e6c52e --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/QueryResponseErrorExtensions.java @@ -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 index 0000000000000..768a439723db4 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ScheduledRuns.java @@ -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 index 0000000000000..2b780f48d8112 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Sensor.java @@ -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 index 0000000000000..dd9606c321e19 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/SensorModel.java @@ -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 index 0000000000000..7176b39c7cdda --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/SensorStatus.java @@ -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 index 0000000000000..125e520255bdc --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Time.java @@ -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 index 0000000000000..07c705d1c714e --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/UnitValue.java @@ -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 index 0000000000000..46e037a8b5059 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/Zone.java @@ -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 index 0000000000000..28c1764e71847 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneNumber.java @@ -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 index 0000000000000..e2030a79ac56a --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneRun.java @@ -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 index 0000000000000..4e662558242cc --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/graphql/dto/ZoneStatus.java @@ -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/HydrawiseLocalApiClient.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/HydrawiseLocalApiClient.java similarity index 93% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseLocalApiClient.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/HydrawiseLocalApiClient.java index df4c5c3e10983..e68bd753f55c0 100644 --- 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/local/HydrawiseLocalApiClient.java @@ -10,22 +10,25 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api; +package org.openhab.binding.hydrawise.internal.api.local; import java.net.URI; -import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.AuthenticationStore; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.util.BasicAuthentication; import org.eclipse.jetty.http.HttpMethod; -import org.openhab.binding.hydrawise.internal.api.model.LocalScheduleResponse; -import org.openhab.binding.hydrawise.internal.api.model.SetZoneResponse; +import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException; +import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException; +import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException; +import org.openhab.binding.hydrawise.internal.api.local.dto.LocalScheduleResponse; +import org.openhab.binding.hydrawise.internal.api.local.dto.SetZoneResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -92,11 +95,12 @@ public void setCredentials(String host, String username, String password) { * @throws HydrawiseConnectionException * @throws HydrawiseAuthenticationException */ + @Nullable public LocalScheduleResponse getLocalSchedule() throws HydrawiseConnectionException, HydrawiseAuthenticationException { String json = doGet(localGetURL); LocalScheduleResponse response = gson.fromJson(json, LocalScheduleResponse.class); - return Objects.requireNonNull(response); + return response; } /** 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/local/HydrawiseZoneCommandBuilder.java similarity index 95% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/HydrawiseZoneCommandBuilder.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/HydrawiseZoneCommandBuilder.java index 4aafec33e9861..4f47964209d96 100644 --- 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/local/HydrawiseZoneCommandBuilder.java @@ -10,7 +10,9 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api; +package org.openhab.binding.hydrawise.internal.api.local; + +import org.eclipse.jdt.annotation.NonNullByDefault; /** * The {@link HydrawiseZoneCommandBuilder} class builds a command URL string to use when sending commands to the @@ -19,6 +21,7 @@ * @author Dan Cunningham - Initial contribution * */ +@NonNullByDefault class HydrawiseZoneCommandBuilder { private final StringBuilder builder; 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/local/dto/BocTopologyActual.java similarity index 90% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/BocTopologyActual.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/BocTopologyActual.java index 06098af7b6d22..96a980df3afec 100644 --- 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/local/dto/BocTopologyActual.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; +package org.openhab.binding.hydrawise.internal.api.local.dto; import java.util.List; 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/local/dto/BocTopologyDesired.java similarity index 90% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/BocTopologyDesired.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/BocTopologyDesired.java index 795594c7db590..f03460a68fd3d 100644 --- 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/local/dto/BocTopologyDesired.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; +package org.openhab.binding.hydrawise.internal.api.local.dto; import java.util.List; 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/local/dto/Controller.java similarity index 93% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Controller.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Controller.java index 2658fa900e436..500cac8105b5d 100644 --- 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/local/dto/Controller.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; +package org.openhab.binding.hydrawise.internal.api.local.dto; import java.util.List; @@ -59,7 +59,5 @@ public class Controller { public String statusIcon; - public Boolean online; - public List 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/local/dto/CustomerDetailsResponse.java similarity index 94% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/CustomerDetailsResponse.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/CustomerDetailsResponse.java index 71db2a37e1d06..6f45014e974fb 100644 --- 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/local/dto/CustomerDetailsResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; +package org.openhab.binding.hydrawise.internal.api.local.dto; import java.util.List; 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/local/dto/Features.java similarity index 96% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Features.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Features.java index cf89a8c621423..56b7d846b6835 100644 --- 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/local/dto/Features.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; +package org.openhab.binding.hydrawise.internal.api.local.dto; import java.util.List; 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/local/dto/Forecast.java similarity index 92% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Forecast.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Forecast.java index ee1034a8766c5..256628b6815bf 100644 --- 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/local/dto/Forecast.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; +package org.openhab.binding.hydrawise.internal.api.local.dto; /** * The {@link Forecast} class models a daily weather forecast 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/local/dto/LocalScheduleResponse.java similarity index 79% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/LocalScheduleResponse.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/LocalScheduleResponse.java index 23dd6ab1338cc..234095c3096c5 100644 --- 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/local/dto/LocalScheduleResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; +package org.openhab.binding.hydrawise.internal.api.local.dto; import java.util.LinkedList; import java.util.List; @@ -22,9 +22,9 @@ */ public class LocalScheduleResponse extends Response { - public List running = new LinkedList<>(); + public List running = new LinkedList(); - public List relays = new LinkedList<>(); + public List relays = new LinkedList(); public String name; 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/local/dto/PlanArray.java similarity index 89% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/PlanArray.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/PlanArray.java index e6be96fb919c5..063bcdce6277d 100644 --- 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/local/dto/PlanArray.java @@ -10,7 +10,9 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; +package org.openhab.binding.hydrawise.internal.api.local.dto; + +import com.google.gson.annotations.SerializedName; /** * The {@link PlanArray} class models am account plan. @@ -63,7 +65,8 @@ public class PlanArray { public String filetypeall; - public String plan_type; + @SerializedName(value = "plan_type") + public String planType; public String pushNotification; 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/local/dto/Relay.java similarity index 73% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Relay.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Relay.java index 3945b470227ee..bbf9521351747 100644 --- 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/local/dto/Relay.java @@ -10,9 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; - -import com.google.gson.annotations.SerializedName; +package org.openhab.binding.hydrawise.internal.api.local.dto; /** * The {@link Relay} class models the Relay response message @@ -23,27 +21,19 @@ public class Relay { public Integer relayId; - public Integer relay; - - public String name; - - public String icon; - - public String lastwater; - public Integer time; public Integer type; - @SerializedName("run") - public String runTime; + public Integer relay; + + public String name; - @SerializedName("run_seconds") - public Integer runTimeSeconds; + public Integer frequency; - public String nicetime; + public String timestr; - public String id; + public Integer runSeconds; /** * Returns back the actual relay number when multiple controllers are chained. 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/local/dto/Response.java similarity index 89% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Response.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Response.java index b6211fcd08477..329c244d0052b 100644 --- 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/local/dto/Response.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; +package org.openhab.binding.hydrawise.internal.api.local.dto; /** * The {@link Response} class models Response messages 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/local/dto/Running.java similarity index 91% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Running.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Running.java index db218e30f1d9a..f7bfe5fd51b26 100644 --- 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/local/dto/Running.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; +package org.openhab.binding.hydrawise.internal.api.local.dto; /** * The {@link Running} class models a running relay 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/local/dto/Sensor.java similarity index 92% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/Sensor.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/Sensor.java index bab38e9232f3a..de95c088b36d9 100644 --- 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/local/dto/Sensor.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; +package org.openhab.binding.hydrawise.internal.api.local.dto; import java.util.List; 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/local/dto/SetControllerResponse.java similarity index 91% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/SetControllerResponse.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/SetControllerResponse.java index a392f3e3d4a45..2f924d2928f4a 100644 --- 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/local/dto/SetControllerResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; +package org.openhab.binding.hydrawise.internal.api.local.dto; /** * The {@link SetControllerResponse} class models the SetController response message 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/local/dto/SetZoneResponse.java similarity index 90% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/SetZoneResponse.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/SetZoneResponse.java index 223ca0ae42f46..44768ee555ab4 100644 --- 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/local/dto/SetZoneResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; +package org.openhab.binding.hydrawise.internal.api.local.dto; /** * The {@link SetZoneResponse} class models the SetZone response message 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/local/dto/StatusScheduleResponse.java similarity index 86% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/model/StatusScheduleResponse.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/api/local/dto/StatusScheduleResponse.java index 65adc2fdabc6a..e506f8b59d834 100644 --- 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/local/dto/StatusScheduleResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal.api.model; +package org.openhab.binding.hydrawise.internal.api.local.dto; import java.util.LinkedList; import java.util.List; @@ -30,7 +30,7 @@ public class StatusScheduleResponse extends LocalScheduleResponse { public Integer nextpoll; - public List sensors = new LinkedList<>(); + public List sensors = new LinkedList(); public String message; @@ -52,7 +52,7 @@ public class StatusScheduleResponse extends LocalScheduleResponse { public String lastContact; - public List forecast = new LinkedList<>(); + public List forecast = new LinkedList(); public String status; 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 index 0000000000000..75c78d681c9d0 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/config/HydrawiseAccountConfiguration.java @@ -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/HydrawiseCloudConfiguration.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/config/HydrawiseControllerConfiguration.java similarity index 54% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseCloudConfiguration.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/config/HydrawiseControllerConfiguration.java index 417497bab23f6..c88a689def9a1 100644 --- 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/config/HydrawiseControllerConfiguration.java @@ -10,27 +10,19 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal; +package org.openhab.binding.hydrawise.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The {@link HydrawiseCloudConfiguration} class contains fields mapping thing configuration parameters. + * The {@link HydrawiseControllerConfiguration} class contains fields mapping thing configuration parameters. * * @author Dan Cunningham - Initial contribution */ -public class HydrawiseCloudConfiguration { - - /** - * Customer API key {@link https://app.hydrawise.com/config/account} - */ - public String apiKey; - - /** - * refresh interval in seconds. - */ - public Integer refresh; - +@NonNullByDefault +public class HydrawiseControllerConfiguration { /** * optional id of the controller to connect to */ - public Integer controllerId; + public Integer controllerId = -1; } 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/config/HydrawiseLocalConfiguration.java similarity index 76% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseLocalConfiguration.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/config/HydrawiseLocalConfiguration.java index a4b26a151a1a6..38fdc02610cc8 100644 --- 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/config/HydrawiseLocalConfiguration.java @@ -10,30 +10,31 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal; +package org.openhab.binding.hydrawise.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; /** * The {@link HydrawiseLocalConfiguration} class contains fields mapping thing configuration parameters. * * @author Dan Cunningham - Initial contribution */ +@NonNullByDefault public class HydrawiseLocalConfiguration { - /** * Host or IP for local controller */ - public String host; + public String host = ""; /** * User name (admin) for local controller */ - public String username; + public String username = ""; /** * Password for local controller */ - public String password; - + public String password = ""; /** * refresh interval in seconds. */ - public int refresh; + public int refresh = 30; } 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 index 0000000000000..f51354eb682b3 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/discovery/HydrawiseCloudControllerDiscoveryService.java @@ -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 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 index 0000000000000..7def5c7f2588a --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseAccountHandler.java @@ -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 controllerListeners = new ArrayList(); + 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> 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 index 0000000000000..eb36de01a8384 --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseControllerHandler.java @@ -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 stateMap = Collections + .synchronizedMap(new HashMap()); + private final Map zoneMaps = Collections + .synchronizedMap(new HashMap()); + 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 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 zones) { + AtomicReference anyRunning = new AtomicReference(false); + AtomicReference anySuspended = new AtomicReference(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 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 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.value, + "\\u00b0F".equals(temperature.unit) ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS)); + } + + private void updateWindspeed(UnitValue wind, String group, String channel) { + updateGroupState(group, channel, new QuantityType(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 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/HydrawiseHandler.java b/bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseLocalHandler.java similarity index 73% rename from bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/HydrawiseHandler.java rename to bundles/org.openhab.binding.hydrawise/src/main/java/org/openhab/binding/hydrawise/internal/handler/HydrawiseLocalHandler.java index 932d888461519..74248fefe03de 100644 --- 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/handler/HydrawiseLocalHandler.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hydrawise.internal; +package org.openhab.binding.hydrawise.internal.handler; import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*; @@ -19,23 +19,27 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException; import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException; import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException; -import org.openhab.binding.hydrawise.internal.api.model.LocalScheduleResponse; -import org.openhab.binding.hydrawise.internal.api.model.Relay; -import org.openhab.binding.hydrawise.internal.api.model.Running; +import org.openhab.binding.hydrawise.internal.api.local.HydrawiseLocalApiClient; +import org.openhab.binding.hydrawise.internal.api.local.dto.LocalScheduleResponse; +import org.openhab.binding.hydrawise.internal.api.local.dto.Relay; +import org.openhab.binding.hydrawise.internal.api.local.dto.Running; +import org.openhab.binding.hydrawise.internal.config.HydrawiseLocalConfiguration; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -49,23 +53,22 @@ import org.slf4j.LoggerFactory; /** - * The {@link HydrawiseHandler} is responsible for handling commands, which are + * The {@link HydrawiseLocalHandler} is responsible for handling commands, which are * sent to one of the channels. * * @author Dan Cunningham - Initial contribution */ @NonNullByDefault -public abstract class HydrawiseHandler extends BaseThingHandler { - - private final Logger logger = LoggerFactory.getLogger(HydrawiseHandler.class); +public class HydrawiseLocalHandler extends BaseThingHandler { + private final Logger logger = LoggerFactory.getLogger(HydrawiseLocalHandler.class); + protected final Map stateMap = Collections.synchronizedMap(new HashMap<>()); + protected final Map relayMap = Collections.synchronizedMap(new HashMap<>()); private @Nullable ScheduledFuture pollFuture; - private Map stateMap = Collections.synchronizedMap(new HashMap<>()); - private Map relayMap = Collections.synchronizedMap(new HashMap<>()); /** * value observed being used by the Hydrawise clients as a max time value, */ - private static long MAX_RUN_TIME = 157680000; + private static final long MAX_RUN_TIME = 157680000; /** * Minimum amount of time we can poll for updates @@ -86,8 +89,28 @@ public abstract class HydrawiseHandler extends BaseThingHandler { * Future to poll for updated */ - public HydrawiseHandler(Thing thing) { + HydrawiseLocalApiClient client; + + public HydrawiseLocalHandler(Thing thing, HttpClient httpClient) { super(thing); + client = new HydrawiseLocalApiClient(httpClient); + } + + @Override + public void initialize() { + scheduler.schedule(this::configureInternal, 0, TimeUnit.SECONDS); + } + + @Override + public void dispose() { + logger.debug("Handler disposed."); + clearPolling(); + } + + @Override + public void channelLinked(ChannelUID channelUID) { + // clear our cached value so the new channel gets updated on the next poll + stateMap.remove(channelUID.getId()); } @SuppressWarnings({ "null", "unused" }) // compiler does not like relayMap.get can return null @@ -120,15 +143,14 @@ public void handleCommand(ChannelUID channelUID, Command command) { clearPolling(); switch (channelId) { case CHANNEL_ZONE_RUN_CUSTOM: - if (!(command instanceof DecimalType)) { + if (!(command instanceof QuantityType)) { logger.warn("Invalid command type for run custom {}", command.getClass().getName()); return; } if (allCommand) { - sendRunAllCommand(((DecimalType) command).intValue()); + client.runAllRelays(((QuantityType) command).intValue()); } else { - Objects.requireNonNull(relay); - sendRunCommand(((DecimalType) command).intValue(), relay); + client.runRelay(((QuantityType) command).intValue(), relay.relay); } break; case CHANNEL_ZONE_RUN: @@ -138,16 +160,15 @@ public void handleCommand(ChannelUID channelUID, Command command) { } if (allCommand) { if (command == OnOffType.ON) { - sendRunAllCommand(); + client.runAllRelays(); } else { - sendStopAllCommand(); + client.stopAllRelays(); } } else { - Objects.requireNonNull(relay); if (command == OnOffType.ON) { - sendRunCommand(relay); + client.runRelay(relay.relay); } else { - sendStopCommand(relay); + client.stopRelay(relay.relay); } } break; @@ -163,46 +184,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - @Override - public void initialize() { - scheduler.schedule(this::configureInternal, 0, TimeUnit.SECONDS); - } - - @Override - public void dispose() { - logger.debug("Handler disposed."); - clearPolling(); - } - - @Override - public void channelLinked(ChannelUID channelUID) { - // clear our cached value so the new channel gets updated on the next poll - stateMap.remove(channelUID.getId()); - } - - protected abstract void configure() - throws NotConfiguredException, HydrawiseConnectionException, HydrawiseAuthenticationException; - - protected abstract void pollController() throws HydrawiseConnectionException, HydrawiseAuthenticationException; - - protected abstract void sendRunCommand(int seconds, Relay relay) - throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException; - - protected abstract void sendRunCommand(Relay relay) - throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException; - - protected abstract void sendStopCommand(Relay relay) - throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException; - - protected abstract void sendRunAllCommand() - throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException; - - protected abstract void sendRunAllCommand(int seconds) - throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException; - - protected abstract void sendStopAllCommand() - throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException; - protected void updateZones(LocalScheduleResponse status) { ZonedDateTime now = ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS); status.relays.forEach(r -> { @@ -211,12 +192,8 @@ protected void updateZones(LocalScheduleResponse status) { logger.trace("Updateing Zone {} {} ", group, r.name); updateGroupState(group, CHANNEL_ZONE_NAME, new StringType(r.name)); updateGroupState(group, CHANNEL_ZONE_TYPE, new DecimalType(r.type)); - updateGroupState(group, CHANNEL_ZONE_TIME, - r.runTimeSeconds != null ? new DecimalType(r.runTimeSeconds) : UnDefType.UNDEF); - String icon = r.icon; - if (icon != null && !icon.isBlank()) { - updateGroupState(group, CHANNEL_ZONE_ICON, new StringType(BASE_IMAGE_URL + icon)); - } + updateGroupState(group, CHANNEL_ZONE_STARTTIME, + r.runSeconds != null ? new QuantityType<>(r.runSeconds, Units.SECOND) : UnDefType.UNDEF); if (r.time >= MAX_RUN_TIME) { updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME, UnDefType.UNDEF); } else { @@ -228,32 +205,21 @@ protected void updateZones(LocalScheduleResponse status) { .filter(z -> Integer.parseInt(z.relayId) == r.relayId.intValue()).findAny(); if (running.isPresent()) { updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.ON); - updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(running.get().timeLeft)); + updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, + new QuantityType<>(running.get().timeLeft, Units.SECOND)); logger.debug("{} Time Left {}", r.name, running.get().timeLeft); } else { updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.OFF); - updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(0)); - + updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new QuantityType<>(0, Units.SECOND)); } updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_RUN, - !status.running.isEmpty() ? OnOffType.ON : OnOffType.OFF); + status.running.size() > 0 ? OnOffType.ON : OnOffType.OFF); }); } - protected void updateGroupState(String group, String channelID, State state) { - String channelName = group + "#" + channelID; - State oldState = stateMap.put(channelName, state); - if (!state.equals(oldState)) { - ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelName); - logger.debug("updateState updating {} {}", channelUID, state); - updateState(channelUID, state); - } - } - @SuppressWarnings("serial") - @NonNullByDefault protected class NotConfiguredException extends Exception { NotConfiguredException(String message) { super(message); @@ -269,11 +235,19 @@ private void configureInternal() { stateMap.clear(); relayMap.clear(); try { - configure(); - initPolling(0); - } catch (NotConfiguredException e) { - logger.debug("Configuration error {}", e.getMessage()); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + HydrawiseLocalConfiguration configuration = getConfig().as(HydrawiseLocalConfiguration.class); + this.refresh = Math.max(configuration.refresh, MIN_REFRESH_SECONDS); + logger.trace("Connecting to host {}", configuration.host); + client.setCredentials(configuration.host, configuration.username, configuration.password); + LocalScheduleResponse response = client.getLocalSchedule(); + if (response != null) { + updateZones(response); + initPolling(refresh); + } else { + logger.debug("Could not connect to service"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Invalid response from service"); + } } catch (HydrawiseConnectionException e) { logger.debug("Could not connect to service"); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); @@ -310,7 +284,10 @@ private void clearPolling() { */ private void pollControllerInternal() { try { - pollController(); + LocalScheduleResponse response = client.getLocalSchedule(); + if (response != null) { + updateZones(response); + } if (getThing().getStatus() != ThingStatus.ONLINE) { updateStatus(ThingStatus.ONLINE); } @@ -324,4 +301,14 @@ private void pollControllerInternal() { 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); + } + } } diff --git a/bundles/org.openhab.binding.hydrawise/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.hydrawise/src/main/resources/OH-INF/thing/channel-types.xml index a7532abfbe901..f6f7fddc866ff 100644 --- a/bundles/org.openhab.binding.hydrawise/src/main/resources/OH-INF/thing/channel-types.xml +++ b/bundles/org.openhab.binding.hydrawise/src/main/resources/OH-INF/thing/channel-types.xml @@ -10,12 +10,14 @@ - - + + + + @@ -25,6 +27,8 @@ + + @@ -34,11 +38,11 @@ - - + + @@ -49,13 +53,43 @@ - + + + + + + + + + + Controller system data + + + + + + + DateTime + + Last contact time of a controller + + + + + String + + Status summary + + + + + String @@ -70,10 +104,17 @@ - - Number + + DateTime - Zone start time in seconds + Next zone start time + + + + + Number:Time + + Next start duration @@ -87,7 +128,7 @@ DateTime - Next time this zone is scheduled to run + The next time this zone is scheduled to run @@ -98,13 +139,25 @@ - Number - - Run zones now for a custom duration of time in seconds + Number:Time + + Run zones now for a custom duration + + + + Switch + + Suspends or resumes zones + + + + DateTime + + Suspends zones until this date - Number + Number:Time Time left that zone will run for @@ -126,22 +179,15 @@ - - Number - - Sensor mode - - - - - Number - - Sensor timer + + Number:Time + + Sensor delay - Number + Number:Time Sensor off timer @@ -160,6 +206,7 @@ Sensor off level + Switch @@ -167,6 +214,13 @@ + + Number:Volume + + Sensor water flow + + + Number:Temperature @@ -198,10 +252,10 @@ - - String - - Day of week for the weather forecast + + DateTime + + Forecast date and time @@ -220,4 +274,25 @@ Wind + + + Number + + Evapotranspiration amount + + + + + Number + + Precipitation amount + + + + + Number + + Probability of precipitation percentage + + diff --git a/bundles/org.openhab.binding.hydrawise/src/main/resources/OH-INF/thing/things.xml b/bundles/org.openhab.binding.hydrawise/src/main/resources/OH-INF/thing/things.xml index b783b67833a86..8f0434e97e31f 100644 --- a/bundles/org.openhab.binding.hydrawise/src/main/resources/OH-INF/thing/things.xml +++ b/bundles/org.openhab.binding.hydrawise/src/main/resources/OH-INF/thing/things.xml @@ -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"> - - - - Hydrawise cloud connected irrigation system + + + Hydrawise account + + + + Your Hydrawise account user name + + + + password + Your Hydrawise account password, for security this will not be saved after the first login attempt + unless the "Save Password" option is enabled + + + + By default, the password will not be persisted after the first login attempt unless this is enabled + false + + + + Specifies the refresh interval in seconds + 60 + + + + + + + + + + Hydrawise connected irrigation controller + + + + + + + Sensor 1 @@ -29,6 +65,8 @@ Sensor 4 + + Today's weather forecast @@ -41,13 +79,13 @@ Day 3 weather forecast - - - Day 4 weather forecast - + + + + Sprinkler Zone 1 @@ -194,19 +232,9 @@ - - - API Key from https://app.hydrawise.com/config/account - - - - Specifies the refresh interval in seconds - 30 - - - - Optional parameter to specify the Hydrawise controller ID if you have more then one associated with - your account. + + + The ID of a cloud connected irrigation controller @@ -214,7 +242,7 @@ - Hydrawise local connected irrigation system + Hydrawise local connected irrigation controller @@ -383,6 +411,4 @@ - - 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 index 0000000000000..6699a943c692d --- /dev/null +++ b/bundles/org.openhab.binding.hydrawise/src/main/resources/query.graphql @@ -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 + } + } + } + } +}