From 6e32b2f1071311257fb84183c541c101b3317373 Mon Sep 17 00:00:00 2001 From: Andrew Schofield Date: Sun, 9 Aug 2020 20:28:31 +0100 Subject: [PATCH] [draytonwiser] Drayton Wiser Binding initial contribution (#3168) Also-by: Hilbrand Bouwkamp Signed-off-by: Andrew Schofield --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../.classpath | 33 + .../org.openhab.binding.draytonwiser/.project | 23 + .../org.openhab.binding.draytonwiser/NOTICE | 13 + .../README.md | 248 +++ .../org.openhab.binding.draytonwiser/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../DraytonWiserBindingConstants.java | 160 ++ .../internal/DraytonWiserHandlerFactory.java | 80 + .../internal/DraytonWiserRefreshListener.java | 27 + .../internal/api/DraytonWiserApi.java | 225 +++ .../api/DraytonWiserApiException.java | 34 + .../DraytonWiserDiscoveryService.java | 209 +++ .../DraytonWiserMDNSDiscoveryParticipant.java | 90 + .../internal/handler/ControllerHandler.java | 166 ++ .../handler/DraytonWiserPropertyHelper.java | 46 + .../handler/DraytonWiserThingHandler.java | 222 +++ .../handler/HeatHubConfiguration.java | 26 + .../internal/handler/HeatHubHandler.java | 170 ++ .../internal/handler/HotWaterHandler.java | 151 ++ .../internal/handler/RoomHandler.java | 194 ++ .../internal/handler/RoomStatHandler.java | 156 ++ .../internal/handler/SmartPlugHandler.java | 148 ++ .../internal/handler/TRVHandler.java | 157 ++ .../internal/model/BoilerSettingsDTO.java | 47 + .../model/DetectedAccessPointDTO.java | 52 + .../internal/model/DeviceDTO.java | 123 ++ .../internal/model/DhcpStatusDTO.java | 60 + .../internal/model/DomainDTO.java | 94 + .../internal/model/DraytonWiserDTO.java | 157 ++ .../internal/model/HeatingChannelDTO.java | 64 + .../internal/model/HotWaterDTO.java | 53 + .../internal/model/LocalDateAndTimeDTO.java | 45 + .../internal/model/NetworkInterfaceDTO.java | 60 + .../internal/model/ReceptionDTO.java | 30 + .../draytonwiser/internal/model/RoomDTO.java | 141 ++ .../internal/model/RoomStatDTO.java | 47 + .../draytonwiser/internal/model/RssiDTO.java | 35 + .../internal/model/SetPointDTO.java | 30 + .../internal/model/SmartPlugDTO.java | 78 + .../internal/model/SmartValveDTO.java | 62 + .../internal/model/StationDTO.java | 157 ++ .../internal/model/SystemDTO.java | 165 ++ .../resources/ESH-INF/binding/binding.xml | 10 + .../resources/ESH-INF/thing/thing-types.xml | 425 +++++ .../DraytonWiserDiscoveryServiceTest.java | 123 ++ .../binding/draytonwiser/internal/test1.json | 1658 +++++++++++++++++ .../binding/draytonwiser/internal/test2.json | 847 +++++++++ bundles/pom.xml | 1 + 50 files changed, 7174 insertions(+) create mode 100644 bundles/org.openhab.binding.draytonwiser/.classpath create mode 100644 bundles/org.openhab.binding.draytonwiser/.project create mode 100644 bundles/org.openhab.binding.draytonwiser/NOTICE create mode 100644 bundles/org.openhab.binding.draytonwiser/README.md create mode 100644 bundles/org.openhab.binding.draytonwiser/pom.xml create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/DraytonWiserBindingConstants.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/DraytonWiserHandlerFactory.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/DraytonWiserRefreshListener.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/api/DraytonWiserApi.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/api/DraytonWiserApiException.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/discovery/DraytonWiserDiscoveryService.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/discovery/DraytonWiserMDNSDiscoveryParticipant.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/ControllerHandler.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/DraytonWiserPropertyHelper.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/DraytonWiserThingHandler.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/HeatHubConfiguration.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/HeatHubHandler.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/HotWaterHandler.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/RoomHandler.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/RoomStatHandler.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/SmartPlugHandler.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/TRVHandler.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/BoilerSettingsDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/DetectedAccessPointDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/DeviceDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/DhcpStatusDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/DomainDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/DraytonWiserDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/HeatingChannelDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/HotWaterDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/LocalDateAndTimeDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/NetworkInterfaceDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/ReceptionDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/RoomDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/RoomStatDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/RssiDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/SetPointDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/SmartPlugDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/SmartValveDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/StationDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/model/SystemDTO.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/resources/ESH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.draytonwiser/src/main/resources/ESH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.draytonwiser/src/test/java/org/openhab/binding/draytonwiser/internal/discovery/DraytonWiserDiscoveryServiceTest.java create mode 100644 bundles/org.openhab.binding.draytonwiser/src/test/resources/org/openhab/binding/draytonwiser/internal/test1.json create mode 100644 bundles/org.openhab.binding.draytonwiser/src/test/resources/org/openhab/binding/draytonwiser/internal/test2.json diff --git a/CODEOWNERS b/CODEOWNERS index 683e5354c0ea1..6844a0443ddb7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -46,6 +46,7 @@ /bundles/org.openhab.binding.dlinksmarthome/ @MikeJMajor /bundles/org.openhab.binding.dmx/ @J-N-K /bundles/org.openhab.binding.doorbird/ @mhilbush +/bundles/org.openhab.binding.draytonwiser/ @andrew-schofield /bundles/org.openhab.binding.dscalarm/ @RSStephens /bundles/org.openhab.binding.dsmr/ @Hilbrand /bundles/org.openhab.binding.ecobee/ @mhilbush diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 07941305d1304..ebd2fd3c4ee7d 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -219,6 +219,11 @@ org.openhab.binding.doorbird ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.draytonwiser + ${project.version} + org.openhab.addons.bundles org.openhab.binding.dscalarm diff --git a/bundles/org.openhab.binding.draytonwiser/.classpath b/bundles/org.openhab.binding.draytonwiser/.classpath new file mode 100644 index 0000000000000..83d1737acf808 --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/.classpath @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.draytonwiser/.project b/bundles/org.openhab.binding.draytonwiser/.project new file mode 100644 index 0000000000000..b39c3da8e218a --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.draytonwiser + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.draytonwiser/NOTICE b/bundles/org.openhab.binding.draytonwiser/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.draytonwiser/README.md b/bundles/org.openhab.binding.draytonwiser/README.md new file mode 100644 index 0000000000000..ef55911140bf3 --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/README.md @@ -0,0 +1,248 @@ +# Drayton Wiser Binding + +This binding integrates the [Drayton Wiser Smart Heating System](https://wiser.draytoncontrols.co.uk/). +The integration happens through the HeatHub, which acts as an IP gateway to the ZigBee devices (thermostats and TRVs). + +## Supported Things + +The Drayton Wiser binding supports the following things: + +| Bridge | Label | Description | +|-----------|---------|------------------------------------------------------------------------------------------------------| +| `heathub` | HeatHub | The network device in the controller that allows us to interact with the other devices in the system | + +| Thing | Label | Description | +|---------------------|-------------------|-------------------------------------------------------------------------------------------| +| `boiler-controller` | Boiler Controller | The _HeatHub_ attached to the boiler. This also acts as the hub device | +| `room` | Room Name | Virtual groups of _Room Thermostats_ and _TRVs_ that can have temperatures and humidities | +| `roomstat` | Thermostat | Wireless thermostats which monitor temperature and humidity, and call for heat | +| `itrv` | iTRV | Wireless TRVs that monitor temperature, alter the radiator valve state and call for heat | +| `hotwater` | Hot Water | Virtual thing to manage hot water states | +| `smart-plug` | Smart Plug | Wireless plug sockets which can be remotely switched | + +## Discovery + +The HeatHub can be discovered automatically via mDNS, however the `secret` cannot be determined automatically. +Once the `secret` has been configured, all other devices can be discovered by triggering device discovery again. + +## Thing Configuration + +### HeatHub Configuration + +Once discovered, the HeatHub `secret` needs to be configured. +There are a few ways to obtain this, assuming you have already configured the system using the Wiser App. + +1. Temporarily install a packet sniffing tool on your mobile device. Every request made includes the `secret` in the header. +2. Enable setup mode on the HeatHub. Connect a machine temporarily to the `WiserHeat_XXXXX` network and browse to `http://192.168.8.1/secret` to obtain the `secret`. + +The `refresh` interval defines in seconds, how often the binding will poll the controller for updates. + +The `awaySetPoint` defines the temperature in degrees Celsius that will be sent to the heathub when away mode is activated. + +## Channels + +### Readonly Channels + +#### Boiler Controller + +| Channel | Item Type | Description | +|------------------------------|----------------------|----------------------------------------------------------| +| `heatingOverride` | Switch | State of the heating override button on the controller | +| `heatChannel1Demand` | Number:Dimensionless | Current demand level of heating channel 1 | +| `heatChannel1DemandState` | Switch | Is channel 1 calling the boiler for heat | +| `heatChannel2Demand` | Number:Dimensionless | Current demand level of heating channel 2 | +| `heatChannel2DemandState` | Switch | Is channel 2 calling the boiler for heat | +| `currentSignalRSSI` | Number | Relative Signal Strength Indicator | +| `currentWiserSignalStrength` | String | Human readable signal strength | +| `currentSignalStrength` | Number | Signal strength value that maps to qualityofservice icon | + +#### Hot Water + +| Channel | Item Type | Description | +|--------------------------|--------------|----------------------------------------------------------| +| `hotWaterOverride` | Switch | State of the hot water override button on the controller | +| `hotWaterDemandState` | Switch | Is hot water calling the boiler for heat | +| `hotWaterBoosted` | Switch | Is hot water currently being boosted | +| `hotWaterBoostRemaining` | Number:Time | How long until the boost deactivates in minutes | + +#### Room + +| Channel | Item Type | Description | +|----------------------|----------------------|------------------------------------------------------------------------------| +| `currentTemperature` | Number:Temperature | Currently reported temperature | +| `currentHumidity` | Number:Dimensionless | Currently reported humidity (if there is a room stat configured in this room | +| `currentDemand` | Number:Dimensionless | Current heat demand percentage of the room | +| `heatRequest` | Switch | Is the room actively requesting heat from the controller | +| `roomBoosted` | Switch | Is the room currently being boosted | +| `roomBoostRemaining` | Number:Time | How long until the boost deactivates in minutes | +| `windowState` | Contact | Is the window open or closed? | + +#### Room Stat + +| Channel | Item Type | Description | +|------------------------------|--------------------------|----------------------------------------------------------| +| `currentTemperature` | Number:Temperature | Currently reported temperature | +| `currentHumidity` | Number:Dimensionless | Currently reported humidity | +| `currentSetPoint` | Number:Temperature | Currently reported set point | +| `currentBatteryVoltage` | Number:ElectricPotential | Currently reported battery voltage | +| `currentWiserBatteryLevel` | String | Human readable battery level | +| `currentBatteryLevel` | Number | Battery level in percent | +| `currentSignalRSSI` | Number | Relative Signal Strength Indicator | +| `currentSignalLQI` | Number | Link Quality Indicator | +| `currentWiserSignalStrength` | String | Human readable signal strength | +| `currentSignalStrength` | Number | Signal strength value that maps to qualityofservice icon | +| `zigbeeConnected` | Switch | Is the roomstat joined to network | + +#### Smart TRV + +| Channel | Item Type | Description | +|------------------------------|--------------------------|----------------------------------------------------------| +| `currentTemperature` | Number:Temperature | Currently reported temperature | +| `currentDemand` | Number:Dimensionless | Current heat demand percentage of the TRV | +| `currentSetPoint` | Number:Temperature | Currently reported set point | +| `currentBatteryVoltage` | Number:ElectricPotential | Currently reported battery voltage | +| `currentWiserBatteryLevel` | String | Human readable battery level | +| `currentBatteryLevel` | Number | Battery level in percent | +| `currentSignalRSSI` | Number | Relative Signal Strength Indicator | +| `currentSignalLQI` | Number | Link Quality Indicator | +| `currentWiserSignalStrength` | String | Human readable signal strength | +| `currentSignalStrength` | Number | Signal strength value that maps to qualityofservice icon | +| `zigbeeConnected` | Switch | Is the TRV joined to network | + +#### Smart Plug + +| Channel | Item Type | Description | +|---------------------|-----------|------------------------------------| +| `currentSignalRSSI` | Number | Relative Signal Strength Indicator | +| `currentSignalLQI` | Number | Link Quality Indicator | +| `zigbeeConnected` | Switch | Is the TRV joined to network | + +### Command Channels + +#### Boiler Controller + +| Channel | Item Type | Description | +|-----------------|-----------|----------------------------| +| `awayModeState` | Switch | Has away mode been enabled | +| `ecoModeState` | Switch | Has eco mode been enabled | + +#### Hot Water + +| Channel | Item Type | Description | +|-------------------------|-----------|--------------------------------------------| +| `manualModeState` | Switch | Has manual mode been enabled | +| `hotWaterSetPoint` | Switch | The current hot water setpoint (on or off) | +| `hotWaterBoostDuration` | Number | Period in hours to boost the hot water | + +#### Room + +| Channel | Item Type | Description | +|------------------------|--------------------|------------------------------------------------| +| `currentSetPoint` | Number:Temperature | The current set point temperature for the room | +| `manualModeState` | Switch | Has manual mode been enabled | +| `roomBoostDuration` | Number | Period in hours to boost the room temperature | +| `windowStateDetection` | Switch | Detect whether windows are open | + +#### Room Stat + +| Channel | Item Type | Description | +|----------------|-----------|----------------------------------| +| `deviceLocked` | Switch | Is the roomstat interface locked | + +#### Smart TRV + +| Channel | Item Type | Description | +|----------------|-----------|-----------------------------| +| `deviceLocked` | Switch | Are the TRV controls locked | + +#### Smart Plug + +| Channel | Item Type | Description | +|-------------------|-----------|----------------------------------------------| +| `plugOutputState` | Switch | The current on/off state of the smart plug | +| `plugAwayAction` | Switch | Should the plug switch off when in away mode | +| `manualModeState` | Switch | Has manual mode been enabled | +| `deviceLocked` | Switch | Are the Smart Plug controls locked | + +#### Known string responses for specific channels: + +| Channel | Known responses | +|------------------------------|--------------------------------------------------------------------| +| `currentWiserSignalStrength` | `{ "VeryGood", "Good", "Medium", "Poor", "NoSignal" }` | +| `currentWiserBatteryLevel` | `{ "Full", "Normal", "TwoThirds", "OneThird", "Low", "Critical" }` | + +## Full Example + +### .things file + +``` +Bridge draytonwiser:heathub:HeatHub [ networkAddress="192.168.1.X", refresh=60, secret="secret from hub", awaySetPoint=10 ] { + boiler-controller controller "Controller" + room livingroom "Living Room" [ name="Living Room" ] + room bathroom "Bathroom" [ name="Bathroom" ] + room bedroom "Bedroom" [ name="Bedroom" ] + roomstat livingroomstat "Living Room Thermostat" [ serialNumber="ABCDEF1234" ] + itrv livingroomtrv "Living Room - TRV" [ serialNumber="ABCDEF1235" ] + hotwater hotwater "Hot Water" + smart-plug tvplug "TV" [ serialNumber="ABCDEF1236" ] +} +``` + +The `name` corresponds to the room name configured in the Wiser App. +It is not case sensitive. +The `serialNumber` corresponds to the device serial number which can be found on a sticker inside the battery compartment of the Smart Valves/TRVs, and behind the wall mount of the Room Thermostats. + +### .items file + +``` +Switch Heating_Override "Heating Override" (gHouse) { channel="draytonwiser:boiler-controller:HeatHub:controller:heatingOverride" } +Number Heating_Demand "Heating Demand [%.0f %%]" (gHouse) { channel="draytonwiser:boiler-controller:HeatHub:controller:heatChannel1Demand" } +Switch Heating_Requesting_Heat "Heating On" (gHouse) { channel="draytonwiser:boiler-controller:HeatHub:controller:heatChannel1DemandState" } +Switch Heating_Away_Mode "Away Mode" (gHouse) { channel="draytonwiser:boiler-controller:HeatHub:controller:awayModeState" } +Switch Heating_Eco_Mode "Eco Mode" (gHouse) { channel="draytonwiser:boiler-controller:HeatHub:controller:ecoModeState" } + +/* Heating */ +Switch Heating_GF_Living "Heating" (GF_Living, Heating) ["Heat Request"] { channel="draytonwiser:room:HeatHub:livingroom:heatRequest" } + +/* Indoor Temperatures */ +Number:Temperature livingroom_temperature "Temperature [%.1f °C]" (GF_Living, Temperature) ["Temperature"] {channel="draytonwiser:room:HeatHub:livingroom:currentTemperature"} + +/* Setpoint Temperatures */ +Number:Temperature livingroom_setpoint "Set Point [%.1f °C]" (GF_Living) ["Set Point"] {channel="draytonwiser:room:HeatHub:livingroom:currentSetPoint"} + +/* Heat Demand */ +Number livingroom_heatdemand "Heat Demand [%.0f %%]" (GF_Living) ["Heat Demand"] {channel="draytonwiser:room:HeatHub:livingroom:currentDemand"} + +/* Manual Mode */ +Switch ManualMode_GF_Living "Manual Mode" (GF_Living) ["Manual Mode"] { channel="draytonwiser:room:HeatHub:livingroom:manualModeState" } + +/* Boost Mode */ +Switch BoostMode_GF_Living "Boosted" (GF_Living) ["Boost Mode"] { channel="draytonwiser:room:HeatHub:livingroom:roomBoosted" } + +/* Boost Duration */ +Number BoostDuration_GF_Living "Boost For[]" (GF_Living) ["Boost Duration"] { channel="draytonwiser:room:HeatHub:livingroom:roomBoostDuration" } + +/* Boost Remaining */ +Number BoostRemaining_GF_Living "Boost Remaining" (GF_Living) ["Boost Remaining"] { channel="draytonwiser:room:HeatHub:livingroom:roomBoostRemaining" } + +/* Humidity */ +Number:Humidity livingroom_humidity "Humidity [%.0f %%]" (GF_Living) ["Humidity"] {channel="draytonwiser:room:HeatHub:livingroom:currentHumidity"} + + +``` + +### Sitemap + +``` +Text label="Living Room" icon="sofa" { + Text item=livingroom_temperature + Setpoint item=livingroom_setpoint step=0.5 + Text item=livingroom_humidity + Text item=Heating_GF_Living + Text item=livingroom_heatdemand + Switch item=ManualMode_GF_Living + Text item=BoostMode_GF_Living + Switch item=BoostDuration_GF_Living icon="time" mappings=[0="0", 0.5="0.5", 1="1", 2="2", 3="3"] + Text item=BoostRemaining_GF_Living icon="time" +} +``` diff --git a/bundles/org.openhab.binding.draytonwiser/pom.xml b/bundles/org.openhab.binding.draytonwiser/pom.xml new file mode 100644 index 0000000000000..7807fc154025e --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.8-SNAPSHOT + + + org.openhab.binding.draytonwiser + + openHAB Add-ons :: Bundles :: DraytonWiser Binding + + diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/feature/feature.xml b/bundles/org.openhab.binding.draytonwiser/src/main/feature/feature.xml new file mode 100644 index 0000000000000..004c64d107142 --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.draytonwiser/${project.version} + + diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/DraytonWiserBindingConstants.java b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/DraytonWiserBindingConstants.java new file mode 100644 index 0000000000000..6074e44e961e1 --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/DraytonWiserBindingConstants.java @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2010-2020 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.draytonwiser.internal; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; + +/** + * The {@link DraytonWiserBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Andrew Schofield - Initial contribution + */ +@NonNullByDefault +public class DraytonWiserBindingConstants { + + public static final String BINDING_ID = "draytonwiser"; + + public static final String REFRESH_INTERVAL = "refresh"; + public static final int DEFAULT_REFRESH_SECONDS = 60; + + public static final int OFFLINE_TEMPERATURE = -32768; + + // Web Service Endpoints + public static final String DEVICE_ENDPOINT = "data/domain/Device/"; + public static final String ROOMSTATS_ENDPOINT = "data/domain/RoomStat/"; + public static final String TRVS_ENDPOINT = "data/domain/SmartValve/"; + public static final String ROOMS_ENDPOINT = "data/domain/Room/"; + public static final String HEATCHANNELS_ENDPOINT = "data/domain/HeatingChannel/"; + public static final String SYSTEM_ENDPOINT = "data/domain/System/"; + public static final String STATION_ENDPOINT = "data/network/Station/"; + public static final String DOMAIN_ENDPOINT = "data/domain/"; + public static final String HOTWATER_ENDPOINT = "data/domain/HotWater/"; + public static final String SMARTPLUG_ENDPOINT = "data/domain/SmartPlug/"; + + // bridge + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "heathub"); + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_CONTROLLER = new ThingTypeUID(BINDING_ID, "boiler-controller"); + public static final ThingTypeUID THING_TYPE_ROOM = new ThingTypeUID(BINDING_ID, "room"); + public static final ThingTypeUID THING_TYPE_ROOMSTAT = new ThingTypeUID(BINDING_ID, "roomstat"); + public static final ThingTypeUID THING_TYPE_ITRV = new ThingTypeUID(BINDING_ID, "itrv"); + public static final ThingTypeUID THING_TYPE_HOTWATER = new ThingTypeUID(BINDING_ID, "hotwater"); + public static final ThingTypeUID THING_TYPE_SMARTPLUG = new ThingTypeUID(BINDING_ID, "smart-plug"); + + // properties + public static final String PROP_ADDRESS = "networkAddress"; + public static final String PROP_SERIAL_NUMBER = "serialNumber"; + public static final String PROP_NAME = "name"; + public static final String PROP_ID = "id"; + + // List of all Channel ids + public static final String CHANNEL_CURRENT_TEMPERATURE = "currentTemperature"; + public static final String CHANNEL_CURRENT_HUMIDITY = "currentHumidity"; + public static final String CHANNEL_CURRENT_SETPOINT = "currentSetPoint"; + public static final String CHANNEL_CURRENT_BATTERY_VOLTAGE = "currentBatteryVoltage"; + public static final String CHANNEL_CURRENT_BATTERY_LEVEL = "currentBatteryLevel"; + public static final String CHANNEL_CURRENT_WISER_BATTERY_LEVEL = "currentWiserBatteryLevel"; + public static final String CHANNEL_CURRENT_DEMAND = "currentDemand"; + public static final String CHANNEL_HEAT_REQUEST = "heatRequest"; + public static final String CHANNEL_CURRENT_SIGNAL_RSSI = "currentSignalRSSI"; + public static final String CHANNEL_CURRENT_SIGNAL_LQI = "currentSignalLQI"; + public static final String CHANNEL_CURRENT_SIGNAL_STRENGTH = "currentSignalStrength"; + public static final String CHANNEL_CURRENT_WISER_SIGNAL_STRENGTH = "currentWiserSignalStrength"; + public static final String CHANNEL_HEATING_OVERRIDE = "heatingOverride"; + public static final String CHANNEL_HOT_WATER_OVERRIDE = "hotWaterOverride"; + public static final String CHANNEL_HEATCHANNEL_1_DEMAND = "heatChannel1Demand"; + public static final String CHANNEL_HEATCHANNEL_2_DEMAND = "heatChannel2Demand"; + public static final String CHANNEL_HEATCHANNEL_1_DEMAND_STATE = "heatChannel1DemandState"; + public static final String CHANNEL_HEATCHANNEL_2_DEMAND_STATE = "heatChannel2DemandState"; + public static final String CHANNEL_HOTWATER_DEMAND_STATE = "hotWaterDemandState"; + public static final String CHANNEL_AWAY_MODE_STATE = "awayModeState"; + public static final String CHANNEL_ECO_MODE_STATE = "ecoModeState"; + public static final String CHANNEL_MANUAL_MODE_STATE = "manualModeState"; + public static final String CHANNEL_ZIGBEE_CONNECTED = "zigbeeConnected"; + public static final String CHANNEL_HOT_WATER_SETPOINT = "hotWaterSetPoint"; + public static final String CHANNEL_HOT_WATER_BOOST_DURATION = "hotWaterBoostDuration"; + public static final String CHANNEL_HOT_WATER_BOOSTED = "hotWaterBoosted"; + public static final String CHANNEL_HOT_WATER_BOOST_REMAINING = "hotWaterBoostRemaining"; + public static final String CHANNEL_ROOM_BOOST_DURATION = "roomBoostDuration"; + public static final String CHANNEL_ROOM_BOOSTED = "roomBoosted"; + public static final String CHANNEL_ROOM_BOOST_REMAINING = "roomBoostRemaining"; + public static final String CHANNEL_ROOM_WINDOW_STATE_DETECTION = "windowStateDetection"; + public static final String CHANNEL_ROOM_WINDOW_STATE = "windowState"; + public static final String CHANNEL_DEVICE_LOCKED = "deviceLocked"; + public static final String CHANNEL_SMARTPLUG_OUTPUT_STATE = "plugOutputState"; + public static final String CHANNEL_SMARTPLUG_AWAY_ACTION = "plugAwayAction"; + + public static final Set SUPPORTED_THING_TYPES_UIDS = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList(THING_TYPE_CONTROLLER, THING_TYPE_ROOM, THING_TYPE_ROOMSTAT, + THING_TYPE_BRIDGE, THING_TYPE_ITRV, THING_TYPE_HOTWATER, THING_TYPE_SMARTPLUG))); + + // Lookups from text representations to useful values + + public enum SignalStrength { + VERYGOOD(4), + GOOD(3), + MEDIUM(2), + POOR(1), + NOSIGNAL(0); + + private final int signalStrength; + + SignalStrength(final int signalStrength) { + this.signalStrength = signalStrength; + } + + public static State toSignalStrength(final String strength) { + try { + return new DecimalType(SignalStrength.valueOf(strength.toUpperCase()).signalStrength); + } catch (final IllegalArgumentException e) { + // Catch unrecognized values. + return UnDefType.UNDEF; + } + } + } + + public enum BatteryLevel { + FULL(100), + NORMAL(80), + TWOTHIRDS(60), + ONETHIRD(40), + LOW(20), + CRITICAL(0); + + private final int batteryLevel; + + private BatteryLevel(final int batteryLevel) { + this.batteryLevel = batteryLevel; + } + + public static State toBatteryLevel(final String level) { + try { + return new DecimalType(BatteryLevel.valueOf(level.toUpperCase()).batteryLevel); + } catch (final IllegalArgumentException e) { + // Catch unrecognized values. + return UnDefType.UNDEF; + } + } + } +} diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/DraytonWiserHandlerFactory.java b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/DraytonWiserHandlerFactory.java new file mode 100644 index 0000000000000..6724d3989284c --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/DraytonWiserHandlerFactory.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2020 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.draytonwiser.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.eclipse.smarthome.io.net.http.HttpClientFactory; +import org.openhab.binding.draytonwiser.internal.handler.ControllerHandler; +import org.openhab.binding.draytonwiser.internal.handler.HeatHubHandler; +import org.openhab.binding.draytonwiser.internal.handler.HotWaterHandler; +import org.openhab.binding.draytonwiser.internal.handler.RoomHandler; +import org.openhab.binding.draytonwiser.internal.handler.RoomStatHandler; +import org.openhab.binding.draytonwiser.internal.handler.SmartPlugHandler; +import org.openhab.binding.draytonwiser.internal.handler.TRVHandler; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link DraytonWiserHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Andrew Schofield - Initial contribution + */ +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.draytonwiser") +@NonNullByDefault +public class DraytonWiserHandlerFactory extends BaseThingHandlerFactory { + + private final HttpClient httpClient; + + @Activate + public DraytonWiserHandlerFactory(@Reference final HttpClientFactory factory) { + httpClient = factory.getCommonHttpClient(); + } + + @Override + public boolean supportsThingType(final ThingTypeUID thingTypeUID) { + return DraytonWiserBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(final Thing thing) { + final ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (DraytonWiserBindingConstants.THING_TYPE_BRIDGE.equals(thingTypeUID)) { + return new HeatHubHandler((Bridge) thing, httpClient); + } else if (DraytonWiserBindingConstants.THING_TYPE_ROOM.equals(thingTypeUID)) { + return new RoomHandler(thing); + } else if (DraytonWiserBindingConstants.THING_TYPE_ROOMSTAT.equals(thingTypeUID)) { + return new RoomStatHandler(thing); + } else if (DraytonWiserBindingConstants.THING_TYPE_ITRV.equals(thingTypeUID)) { + return new TRVHandler(thing); + } else if (DraytonWiserBindingConstants.THING_TYPE_CONTROLLER.equals(thingTypeUID)) { + return new ControllerHandler(thing); + } else if (DraytonWiserBindingConstants.THING_TYPE_HOTWATER.equals(thingTypeUID)) { + return new HotWaterHandler(thing); + } else if (DraytonWiserBindingConstants.THING_TYPE_SMARTPLUG.equals(thingTypeUID)) { + return new SmartPlugHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/DraytonWiserRefreshListener.java b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/DraytonWiserRefreshListener.java new file mode 100644 index 0000000000000..773b93770be2d --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/DraytonWiserRefreshListener.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2020 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.draytonwiser.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.draytonwiser.internal.model.DraytonWiserDTO; + +/** + * Listener for item/sensor updates. + * + * @author Andrew Schofield - Initial contribution + */ +@NonNullByDefault +public interface DraytonWiserRefreshListener { + + void onRefresh(DraytonWiserDTO domain); +} diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/api/DraytonWiserApi.java b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/api/DraytonWiserApi.java new file mode 100644 index 0000000000000..8c03af39f90f9 --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/api/DraytonWiserApi.java @@ -0,0 +1,225 @@ +/** + * Copyright (c) 2010-2020 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.draytonwiser.internal.api; + +import static org.openhab.binding.draytonwiser.internal.DraytonWiserBindingConstants.*; + +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.ContentResponse; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.draytonwiser.internal.handler.HeatHubConfiguration; +import org.openhab.binding.draytonwiser.internal.model.DomainDTO; +import org.openhab.binding.draytonwiser.internal.model.StationDTO; +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.JsonSyntaxException; + +/** + * Class with api specific call code. + * + * @author Andrew Schofield - Initial contribution + * @author Hilbrand Bouwkamp - Moved Api specific code to it's own class + */ +@NonNullByDefault +public class DraytonWiserApi { + + public static final Gson GSON = new GsonBuilder().setFieldNamingStrategy(FieldNamingPolicy.UPPER_CAMEL_CASE) + .create(); + + private final Logger logger = LoggerFactory.getLogger(DraytonWiserApi.class); + private final HttpClient httpClient; + + private HeatHubConfiguration configuration = new HeatHubConfiguration(); + private int failCount; + + public DraytonWiserApi(final HttpClient httpClient) { + this.httpClient = httpClient; + } + + public void setConfiguration(final HeatHubConfiguration configuration) { + this.configuration = configuration; + } + + public @Nullable StationDTO getStation() throws DraytonWiserApiException { + final ContentResponse response = sendMessageToHeatHub(STATION_ENDPOINT, HttpMethod.GET); + + return response == null ? null : GSON.fromJson(response.getContentAsString(), StationDTO.class); + } + + public @Nullable DomainDTO getDomain() throws DraytonWiserApiException { + final ContentResponse response = sendMessageToHeatHub(DOMAIN_ENDPOINT, HttpMethod.GET); + + if (response == null) { + return null; + } + + try { + return GSON.fromJson(response.getContentAsString(), DomainDTO.class); + } catch (final JsonSyntaxException e) { + logger.debug("Could not parse Json content: {}", e.getMessage(), e); + return null; + } + } + + public void setRoomSetPoint(final int roomId, final int setPoint) throws DraytonWiserApiException { + final String payload = "{\"RequestOverride\":{\"Type\":\"Manual\", \"SetPoint\":" + setPoint + "}}"; + + sendMessageToHeatHub(ROOMS_ENDPOINT + roomId, "PATCH", payload); + } + + public void setRoomManualMode(final int roomId, final boolean manualMode) throws DraytonWiserApiException { + String payload = "{\"Mode\":\"" + (manualMode ? "Manual" : "Auto") + "\"}"; + sendMessageToHeatHub(ROOMS_ENDPOINT + roomId, "PATCH", payload); + payload = "{\"RequestOverride\":{\"Type\":\"None\",\"Originator\" :\"App\",\"DurationMinutes\":0,\"SetPoint\":0}}"; + sendMessageToHeatHub(ROOMS_ENDPOINT + roomId, "PATCH", payload); + } + + public void setRoomWindowStateDetection(final int roomId, final boolean windowStateDetection) + throws DraytonWiserApiException { + final String payload = windowStateDetection ? "true" : "false"; + sendMessageToHeatHub(ROOMS_ENDPOINT + roomId + "/WindowDetectionActive", "PATCH", payload); + } + + public void setRoomBoostActive(final int roomId, final int setPoint, final int duration) + throws DraytonWiserApiException { + final String payload = "{\"RequestOverride\":{\"Type\":\"Manual\",\"Originator\" :\"App\",\"DurationMinutes\":" + + duration + ",\"SetPoint\":" + setPoint + "}}"; + sendMessageToHeatHub(ROOMS_ENDPOINT + roomId, "PATCH", payload); + } + + public void setRoomBoostInactive(final int roomId) throws DraytonWiserApiException { + final String payload = "{\"RequestOverride\":{\"Type\":\"None\",\"Originator\" :\"App\",\"DurationMinutes\":0,\"SetPoint\":0}}"; + sendMessageToHeatHub(ROOMS_ENDPOINT + roomId, "PATCH", payload); + } + + public void setHotWaterManualMode(final boolean manualMode) throws DraytonWiserApiException { + String payload = "{\"Mode\":\"" + (manualMode ? "Manual" : "Auto") + "\"}"; + sendMessageToHeatHub(HOTWATER_ENDPOINT + "2", "PATCH", payload); + payload = "{\"RequestOverride\":{\"Type\":\"None\",\"Originator\" :\"App\",\"DurationMinutes\":0,\"SetPoint\":0}}"; + sendMessageToHeatHub(HOTWATER_ENDPOINT + "2", "PATCH", payload); + } + + public void setHotWaterSetPoint(final int setPoint) throws DraytonWiserApiException { + final String payload = "{\"RequestOverride\":{\"Type\":\"Manual\", \"SetPoint\":" + setPoint + "}}"; + sendMessageToHeatHub(HOTWATER_ENDPOINT + "2", "PATCH", payload); + } + + public void setHotWaterBoostActive(final int duration) throws DraytonWiserApiException { + final String payload = "{\"RequestOverride\":{\"Type\":\"Manual\",\"Originator\" :\"App\",\"DurationMinutes\":" + + duration + ",\"SetPoint\":1100}}"; + sendMessageToHeatHub(HOTWATER_ENDPOINT + "2", "PATCH", payload); + } + + public void setHotWaterBoostInactive() throws DraytonWiserApiException { + final String payload = "{\"RequestOverride\":{\"Type\":\"None\",\"Originator\" :\"App\",\"DurationMinutes\":0,\"SetPoint\":0}}"; + sendMessageToHeatHub(HOTWATER_ENDPOINT + "2", "PATCH", payload); + } + + public void setAwayMode(final boolean awayMode) throws DraytonWiserApiException { + final int setPoint = configuration.awaySetPoint * 10; + + String payload = "{\"Type\":" + (awayMode ? "2" : "0") + ", \"setPoint\":" + (awayMode ? setPoint : "0") + "}"; + sendMessageToHeatHub(SYSTEM_ENDPOINT + "RequestOverride", "PATCH", payload); + payload = "{\"Type\":" + (awayMode ? "2" : "0") + ", \"setPoint\":" + (awayMode ? "-200" : "0") + "}"; + sendMessageToHeatHub(HOTWATER_ENDPOINT + "2/RequestOverride", "PATCH", payload); + } + + public void setDeviceLocked(final int deviceId, final boolean locked) throws DraytonWiserApiException { + final String payload = locked ? "true" : "false"; + sendMessageToHeatHub(DEVICE_ENDPOINT + deviceId + "/DeviceLockEnabled", "PATCH", payload); + } + + public void setEcoMode(final boolean ecoMode) throws DraytonWiserApiException { + final String payload = "{\"EcoModeEnabled\":" + ecoMode + "}"; + sendMessageToHeatHub(SYSTEM_ENDPOINT, "PATCH", payload); + } + + public void setSmartPlugManualMode(final int id, final boolean manualMode) throws DraytonWiserApiException { + final String payload = "{\"Mode\":\"" + (manualMode ? "Manual" : "Auto") + "\"}"; + sendMessageToHeatHub(SMARTPLUG_ENDPOINT + id, "PATCH", payload); + } + + public void setSmartPlugOutputState(final int id, final boolean outputState) throws DraytonWiserApiException { + final String payload = "{\"RequestOutput\":\"" + (outputState ? "On" : "Off") + "\"}"; + sendMessageToHeatHub(SMARTPLUG_ENDPOINT + id, "PATCH", payload); + } + + public void setSmartPlugAwayAction(final int id, final boolean awayAction) throws DraytonWiserApiException { + final String payload = "{\"AwayAction\":\"" + (awayAction ? "Off" : "NoChange") + "\"}"; + sendMessageToHeatHub(SMARTPLUG_ENDPOINT + id, "PATCH", payload); + } + + private synchronized @Nullable ContentResponse sendMessageToHeatHub(final String path, final HttpMethod method) + throws DraytonWiserApiException { + return sendMessageToHeatHub(path, method.asString(), ""); + } + + private synchronized @Nullable ContentResponse sendMessageToHeatHub(final String path, final String method, + final String content) throws DraytonWiserApiException { + // we need to keep track of the number of times that the heat hub has "failed" to respond. + // we only actually report a failure if we hit an error state 3 or more times + try { + logger.debug("Sending message to heathub: {}", path); + final StringContentProvider contentProvider = new StringContentProvider(content); + final ContentResponse response = httpClient + .newRequest("http://" + configuration.networkAddress + "/" + path).method(method) + .header("SECRET", configuration.secret).content(contentProvider).timeout(10, TimeUnit.SECONDS) + .send(); + + if (logger.isTraceEnabled()) { + logger.trace("Reponse (Status:{}): {}", response.getStatus(), response.getContentAsString()); + } + if (response.getStatus() == HttpStatus.OK_200) { + failCount = 0; + return response; + } else if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) { + failCount++; + if (failCount > 2) { + throw new DraytonWiserApiException("Invalid authorization token"); + } + } else { + failCount++; + if (failCount > 2) { + throw new DraytonWiserApiException("Heathub didn't repond after " + failCount + " retries"); + } + } + } catch (final TimeoutException e) { + failCount++; + if (failCount > 2) { + logger.debug("Heathub didn't repond in time: {}", e.getMessage()); + throw new DraytonWiserApiException("Heathub didn't repond in time", e); + } + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (final ExecutionException e) { + logger.debug("Execution Exception: {}", e.getMessage(), e); + throw new DraytonWiserApiException(e.getMessage(), e); + } catch (final RuntimeException e) { + logger.debug("Unexpected error: {}", e.getMessage(), e); + throw new DraytonWiserApiException(e.getMessage(), e); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/api/DraytonWiserApiException.java b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/api/DraytonWiserApiException.java new file mode 100644 index 0000000000000..0430fd5e0f0c8 --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/api/DraytonWiserApiException.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2020 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.draytonwiser.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception thrown in case of api problems. + * + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +public class DraytonWiserApiException extends Exception { + + private static final long serialVersionUID = 1L; + + public DraytonWiserApiException(final String message) { + super(message); + } + + public DraytonWiserApiException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/discovery/DraytonWiserDiscoveryService.java b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/discovery/DraytonWiserDiscoveryService.java new file mode 100644 index 0000000000000..78351d379c684 --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/discovery/DraytonWiserDiscoveryService.java @@ -0,0 +1,209 @@ +/** + * Copyright (c) 2010-2020 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.draytonwiser.internal.discovery; + +import static org.openhab.binding.draytonwiser.internal.DraytonWiserBindingConstants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; +import org.eclipse.smarthome.config.discovery.DiscoveryResult; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.openhab.binding.draytonwiser.internal.DraytonWiserRefreshListener; +import org.openhab.binding.draytonwiser.internal.handler.DraytonWiserPropertyHelper; +import org.openhab.binding.draytonwiser.internal.handler.HeatHubHandler; +import org.openhab.binding.draytonwiser.internal.model.DeviceDTO; +import org.openhab.binding.draytonwiser.internal.model.DraytonWiserDTO; +import org.openhab.binding.draytonwiser.internal.model.HotWaterDTO; +import org.openhab.binding.draytonwiser.internal.model.RoomDTO; +import org.openhab.binding.draytonwiser.internal.model.RoomStatDTO; +import org.openhab.binding.draytonwiser.internal.model.SmartPlugDTO; +import org.openhab.binding.draytonwiser.internal.model.SmartValveDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link DraytonWiserDiscoveryService} is used to discover devices that are connected to a Heat Hub. + * + * @author Andrew Schofield - Initial contribution + */ +@NonNullByDefault +public class DraytonWiserDiscoveryService extends AbstractDiscoveryService + implements DiscoveryService, ThingHandlerService, DraytonWiserRefreshListener { + + private final Logger logger = LoggerFactory.getLogger(DraytonWiserDiscoveryService.class); + + private @Nullable HeatHubHandler bridgeHandler; + private @Nullable ThingUID bridgeUID; + + public DraytonWiserDiscoveryService() { + super(SUPPORTED_THING_TYPES_UIDS, 30, false); + } + + @Override + public void activate() { + super.activate(null); + } + + @Override + public void deactivate() { + super.deactivate(); + } + + @Override + protected void startScan() { + final HeatHubHandler handler = bridgeHandler; + if (handler != null) { + removeOlderResults(getTimestampOfLastScan()); + handler.setDiscoveryService(this); + } + } + + @Override + public void onRefresh(final DraytonWiserDTO domainDTOProxy) { + logger.debug("Received data from Drayton Wise device. Parsing to discover devices."); + onControllerAdded(domainDTOProxy); + domainDTOProxy.getRooms().forEach(this::onRoomAdded); + domainDTOProxy.getRoomStats().forEach(r -> onRoomStatAdded(domainDTOProxy, r)); + domainDTOProxy.getSmartValves().forEach(sv -> onSmartValveAdded(domainDTOProxy, sv)); + domainDTOProxy.getHotWater().forEach(hw -> onHotWaterAdded(domainDTOProxy, hw)); + domainDTOProxy.getSmartPlugs().forEach(sp -> onSmartPlugAdded(domainDTOProxy, sp)); + } + + private void onControllerAdded(final DraytonWiserDTO domainDTOProxy) { + final DeviceDTO device = domainDTOProxy.getExtendedDeviceProperties(0); + + if (device != null) { + logger.debug("Controller discovered, model: {}", device.getModelIdentifier()); + onThingWithId(THING_TYPE_CONTROLLER, "controller", device, "Controller"); + } + } + + private void onHotWaterAdded(final DraytonWiserDTO domainDTOProxy, final HotWaterDTO hotWater) { + final Integer hotWaterId = hotWater.getId(); + final String roomName = getRoomName(domainDTOProxy, hotWaterId); + + onThingWithId(THING_TYPE_HOTWATER, "hotwater" + hotWaterId, null, + (roomName.isEmpty() ? "" : (roomName + " - ")) + "Hot Water"); + } + + private void onThingWithId(final ThingTypeUID deviceType, final String deviceTypeId, + @Nullable final DeviceDTO device, final String name) { + logger.debug("{} discovered: {}", deviceTypeId, name); + final Map properties = new HashMap<>(); + + properties.put(PROP_ID, deviceTypeId); + if (device != null) { + DraytonWiserPropertyHelper.setGeneralDeviceProperties(device, properties); + } + final DiscoveryResult discoveryResult = DiscoveryResultBuilder + .create(new ThingUID(deviceType, bridgeUID, deviceTypeId)).withBridge(bridgeUID) + .withProperties(properties).withRepresentationProperty(PROP_ID).withLabel(name).build(); + + thingDiscovered(discoveryResult); + } + + private void onRoomStatAdded(final DraytonWiserDTO domainDTOProxy, final RoomStatDTO roomStat) { + final Integer roomStatId = roomStat.getId(); + final DeviceDTO device = domainDTOProxy.getExtendedDeviceProperties(roomStatId); + + if (device != null) { + onThingWithSerialNumber(THING_TYPE_ROOMSTAT, "Thermostat", device, getRoomName(domainDTOProxy, roomStatId)); + } + } + + private void onRoomAdded(final RoomDTO room) { + final Map properties = new HashMap<>(); + + logger.debug("Room discovered: {}", room.getName()); + properties.put(PROP_NAME, room.getName()); + final DiscoveryResult discoveryResult = DiscoveryResultBuilder + .create(new ThingUID(THING_TYPE_ROOM, bridgeUID, + room.getName().replaceAll("[^A-Za-z0-9]", "").toLowerCase())) + .withBridge(bridgeUID).withProperties(properties).withRepresentationProperty(PROP_NAME) + .withLabel(room.getName()).build(); + + thingDiscovered(discoveryResult); + } + + private void onSmartValveAdded(final DraytonWiserDTO domainDTOProxy, final SmartValveDTO smartValve) { + final Integer smartValueId = smartValve.getId(); + final DeviceDTO device = domainDTOProxy.getExtendedDeviceProperties(smartValueId); + + if (device != null) { + onThingWithSerialNumber(THING_TYPE_ITRV, "TRV", device, getRoomName(domainDTOProxy, smartValueId)); + } + } + + private void onSmartPlugAdded(final DraytonWiserDTO domainDTOProxy, final SmartPlugDTO smartPlug) { + final DeviceDTO device = domainDTOProxy.getExtendedDeviceProperties(smartPlug.getId()); + + if (device != null) { + onThingWithSerialNumber(THING_TYPE_SMARTPLUG, "Smart Plug", device, smartPlug.getName()); + } + } + + private String getRoomName(final DraytonWiserDTO domainDTOProxy, final Integer roomId) { + final RoomDTO assignedRoom = domainDTOProxy.getRoomForDeviceId(roomId); + return assignedRoom == null ? "" : assignedRoom.getName(); + } + + private void onThingWithSerialNumber(final ThingTypeUID deviceType, final String deviceTypeName, + final DeviceDTO device, final String name) { + final String serialNumber = device.getSerialNumber(); + logger.debug("{} discovered, serialnumber: {}", deviceTypeName, serialNumber); + final Map properties = new HashMap<>(); + + DraytonWiserPropertyHelper.setPropertiesWithSerialNumber(device, properties); + final DiscoveryResult discoveryResult = DiscoveryResultBuilder + .create(new ThingUID(deviceType, bridgeUID, serialNumber)).withBridge(bridgeUID) + .withProperties(properties).withRepresentationProperty(PROP_SERIAL_NUMBER) + .withLabel((name.isEmpty() ? "" : (name + " - ")) + deviceTypeName).build(); + + thingDiscovered(discoveryResult); + } + + @Override + public synchronized void stopScan() { + final HeatHubHandler handler = bridgeHandler; + + if (handler != null) { + handler.unsetDiscoveryService(); + } + super.stopScan(); + } + + @Override + public void setThingHandler(@Nullable final ThingHandler handler) { + if (handler instanceof HeatHubHandler) { + bridgeHandler = (HeatHubHandler) handler; + bridgeUID = handler.getThing().getUID(); + } else { + bridgeHandler = null; + bridgeUID = null; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return bridgeHandler; + } +} diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/discovery/DraytonWiserMDNSDiscoveryParticipant.java b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/discovery/DraytonWiserMDNSDiscoveryParticipant.java new file mode 100644 index 0000000000000..007eca1aa52f0 --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/discovery/DraytonWiserMDNSDiscoveryParticipant.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010-2020 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.draytonwiser.internal.discovery; + +import static org.openhab.binding.draytonwiser.internal.DraytonWiserBindingConstants.*; + +import java.net.InetAddress; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.jmdns.ServiceInfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.discovery.DiscoveryResult; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.config.discovery.mdns.MDNSDiscoveryParticipant; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link DraytonWiserMDNSDiscoveryParticipant} is responsible for discovering Drayton Wiser Heat Hubs. It uses the + * central MDNS Discovery Service. + * + * @author Andrew Schofield - Initial contribution + * + */ +@NonNullByDefault +@Component(service = MDNSDiscoveryParticipant.class, configurationPid = "mdnsdiscovery.draytonwiser") +public class DraytonWiserMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant { + + private final Logger logger = LoggerFactory.getLogger(DraytonWiserMDNSDiscoveryParticipant.class); + + @Override + public Set getSupportedThingTypeUIDs() { + return Collections.singleton(THING_TYPE_BRIDGE); + } + + @Override + public String getServiceType() { + return "_http._tcp.local."; + } + + @Override + public @Nullable DiscoveryResult createResult(final ServiceInfo service) { + if (service.getApplication().contains("http")) { + final ThingUID uid = getThingUID(service); + + if (uid != null) { + logger.debug("Discovered Heat Hub '{}' with uid: {}", service.getName(), uid); + final Map properties = new HashMap<>(2); + final InetAddress[] addresses = service.getInetAddresses(); + + if (addresses.length > 0 && addresses[0] != null) { + properties.put(PROP_ADDRESS, addresses[0].getHostAddress()); + properties.put(REFRESH_INTERVAL, DEFAULT_REFRESH_SECONDS); + } + + return DiscoveryResultBuilder.create(uid).withProperties(properties) + .withRepresentationProperty(PROP_ADDRESS).withLabel("Heat Hub - " + service.getName()).build(); + } + } + return null; + } + + @Override + public @Nullable ThingUID getThingUID(final ServiceInfo service) { + if (service.getType() != null && service.getType().equals(getServiceType()) + && service.getName().contains("WiserHeat")) { + logger.trace("Discovered a Drayton Wiser Heat Hub thing with name '{}'", service.getName()); + return new ThingUID(THING_TYPE_BRIDGE, service.getName()); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/ControllerHandler.java b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/ControllerHandler.java new file mode 100644 index 0000000000000..76ca6630e6d8f --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/ControllerHandler.java @@ -0,0 +1,166 @@ +/** + * Copyright (c) 2010-2020 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.draytonwiser.internal.handler; + +import static org.openhab.binding.draytonwiser.internal.DraytonWiserBindingConstants.*; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.draytonwiser.internal.DraytonWiserBindingConstants.SignalStrength; +import org.openhab.binding.draytonwiser.internal.api.DraytonWiserApiException; +import org.openhab.binding.draytonwiser.internal.handler.ControllerHandler.ControllerData; +import org.openhab.binding.draytonwiser.internal.model.DeviceDTO; +import org.openhab.binding.draytonwiser.internal.model.DraytonWiserDTO; +import org.openhab.binding.draytonwiser.internal.model.HeatingChannelDTO; +import org.openhab.binding.draytonwiser.internal.model.HotWaterDTO; +import org.openhab.binding.draytonwiser.internal.model.StationDTO; +import org.openhab.binding.draytonwiser.internal.model.SystemDTO; + +/** + * The {@link ControllerHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Andrew Schofield - Initial contribution + * @author Hilbrand Bouwkamp - Simplified handler to handle null data + */ +@NonNullByDefault +public class ControllerHandler extends DraytonWiserThingHandler { + + public ControllerHandler(final Thing thing) { + super(thing); + } + + @Override + protected void handleCommand(final String channelId, final Command command) throws DraytonWiserApiException { + if (command instanceof OnOffType) { + final boolean onOffState = OnOffType.ON.equals(command); + + if (CHANNEL_AWAY_MODE_STATE.equals(channelId)) { + setAwayMode(onOffState); + } else if (CHANNEL_ECO_MODE_STATE.equals(channelId)) { + setEcoMode(onOffState); + } + } + } + + @Override + protected void refresh() { + updateState(CHANNEL_HEATING_OVERRIDE, this::getHeatingOverride); + updateState(CHANNEL_CURRENT_SIGNAL_RSSI, this::getRSSI); + updateState(CHANNEL_CURRENT_WISER_SIGNAL_STRENGTH, this::getWiserSignalStrength); + updateState(CHANNEL_CURRENT_SIGNAL_STRENGTH, this::getSignalStrength); + updateState(CHANNEL_HEATCHANNEL_1_DEMAND, this::getHeatChannel1Demand); + updateState(CHANNEL_HEATCHANNEL_2_DEMAND, this::getHeatChannel2Demand); + updateState(CHANNEL_HEATCHANNEL_1_DEMAND_STATE, this::getHeatChannel1DemandState); + updateState(CHANNEL_HEATCHANNEL_2_DEMAND_STATE, this::getHeatChannel2DemandState); + updateState(CHANNEL_AWAY_MODE_STATE, this::getAwayModeState); + updateState(CHANNEL_ECO_MODE_STATE, this::getEcoModeState); + } + + @Override + protected @Nullable ControllerData collectData(final DraytonWiserDTO domainDTOProxy) + throws DraytonWiserApiException { + final StationDTO station = getApi().getStation(); + final DeviceDTO device = domainDTOProxy.getExtendedDeviceProperties(0); + final SystemDTO system = domainDTOProxy.getSystem(); + final List heatingChannels = domainDTOProxy.getHeatingChannels(); + final List hotWaterChannels = domainDTOProxy.getHotWater(); + + return station != null && device != null && system != null + ? new ControllerData(device, system, station, heatingChannels, hotWaterChannels) + : null; + } + + private State getHeatingOverride() { + return OnOffType.from("ON".equalsIgnoreCase(getData().system.getHeatingButtonOverrideState())); + } + + private State getRSSI() { + return new DecimalType(getData().station.getRSSI().getCurrent()); + } + + private State getWiserSignalStrength() { + return new StringType(getData().device.getDisplayedSignalStrength()); + } + + private State getSignalStrength() { + return SignalStrength.toSignalStrength(getData().device.getDisplayedSignalStrength()); + } + + private State getHeatChannel1Demand() { + return getData().heatingChannels.size() >= 1 + ? new QuantityType<>(getData().heatingChannels.get(0).getPercentageDemand(), SmartHomeUnits.PERCENT) + : UnDefType.UNDEF; + } + + private State getHeatChannel2Demand() { + return getData().heatingChannels.size() >= 2 + ? new QuantityType<>(getData().heatingChannels.get(1).getPercentageDemand(), SmartHomeUnits.PERCENT) + : UnDefType.UNDEF; + } + + private State getHeatChannel1DemandState() { + return OnOffType.from(getData().heatingChannels.size() >= 1 + && "ON".equalsIgnoreCase(getData().heatingChannels.get(0).getHeatingRelayState())); + } + + private State getHeatChannel2DemandState() { + return OnOffType.from(getData().heatingChannels.size() >= 2 + && "ON".equalsIgnoreCase(getData().heatingChannels.get(1).getHeatingRelayState())); + } + + private State getAwayModeState() { + return OnOffType.from(getData().system.getOverrideType() != null + && "AWAY".equalsIgnoreCase(getData().system.getOverrideType())); + } + + private State getEcoModeState() { + return OnOffType.from(getData().system.getEcoModeEnabled() != null && getData().system.getEcoModeEnabled()); + } + + private void setAwayMode(final Boolean awayMode) throws DraytonWiserApiException { + getApi().setAwayMode(awayMode); + } + + private void setEcoMode(final Boolean ecoMode) throws DraytonWiserApiException { + getApi().setEcoMode(ecoMode); + } + + static class ControllerData { + public final DeviceDTO device; + public final SystemDTO system; + public final StationDTO station; + public final List heatingChannels; + public final List hotWaterChannels; + + public ControllerData(final DeviceDTO device, final SystemDTO system, final StationDTO station, + final List heatingChannels, final List hotWaterChannels) { + this.device = device; + this.system = system; + this.station = station; + this.heatingChannels = heatingChannels; + this.hotWaterChannels = hotWaterChannels; + } + } +} diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/DraytonWiserPropertyHelper.java b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/DraytonWiserPropertyHelper.java new file mode 100644 index 0000000000000..4b2c4d2bf790c --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/DraytonWiserPropertyHelper.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2020 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.draytonwiser.internal.handler; + +import static org.openhab.binding.draytonwiser.internal.DraytonWiserBindingConstants.PROP_SERIAL_NUMBER; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.draytonwiser.internal.model.DeviceDTO; + +/** + * + * @author Andrew Schofield - Initial contribution + * @author Hilbrand Bouwkamp - Put all device property setting in a separate class + */ +@NonNullByDefault +public final class DraytonWiserPropertyHelper { + + private DraytonWiserPropertyHelper() { + // helper class + } + + public static void setPropertiesWithSerialNumber(final DeviceDTO device, final Map properties) { + properties.put(PROP_SERIAL_NUMBER, device.getSerialNumber()); + setGeneralDeviceProperties(device, properties); + } + + public static void setGeneralDeviceProperties(final DeviceDTO device, + final Map properties) { + properties.put("Device Type", device.getProductIdentifier()); + properties.put("Firmware Version", device.getActiveFirmwareVersion()); + properties.put("Manufacturer", device.getManufacturer()); + properties.put("Model", device.getModelIdentifier()); + } +} diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/DraytonWiserThingHandler.java b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/DraytonWiserThingHandler.java new file mode 100644 index 0000000000000..1ab4483234295 --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/DraytonWiserThingHandler.java @@ -0,0 +1,222 @@ +/** + * Copyright (c) 2010-2020 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.draytonwiser.internal.handler; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.ThingStatusInfo; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.thing.util.ThingHandlerHelper; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.draytonwiser.internal.DraytonWiserRefreshListener; +import org.openhab.binding.draytonwiser.internal.api.DraytonWiserApi; +import org.openhab.binding.draytonwiser.internal.api.DraytonWiserApiException; +import org.openhab.binding.draytonwiser.internal.model.DraytonWiserDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link DraytonWiserThingHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Andrew Schofield - Initial contribution + * @author Hilbrand Bouwkamp - Moved generic code from subclasses to this class + */ +@NonNullByDefault +abstract class DraytonWiserThingHandler extends BaseThingHandler implements DraytonWiserRefreshListener { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private @Nullable DraytonWiserApi api; + private @Nullable T data; + private @Nullable DraytonWiserDTO draytonWiseDTO; + private @Nullable ScheduledFuture handleCommandRefreshFuture; + + protected DraytonWiserThingHandler(final Thing thing) { + super(thing); + } + + @Override + public void initialize() { + final HeatHubHandler bridgeHandler = getHeatHubHandler(); + + if (bridgeHandler == null) { + api = null; + } else { + api = bridgeHandler.getApi(); + updateStatus(ThingStatus.UNKNOWN); + } + } + + @Override + public final void handleCommand(final ChannelUID channelUID, final Command command) { + final HeatHubHandler heatHubHandler = getHeatHubHandler(); + + if (heatHubHandler == null) { + return; // if null status will be updated to offline + } + if (command instanceof RefreshType) { + heatHubHandler.refresh(); + } else { + final DraytonWiserApi api = this.api; + + if (api != null && data != null) { + try { + handleCommand(channelUID.getId(), command); + // cancel previous refresh, but wait for it to finish, so no forced cancel + disposehandleCommandRefreshFuture(false); + // update the state after the heathub has had time to react + handleCommandRefreshFuture = scheduler.schedule(heatHubHandler::refresh, 5, TimeUnit.SECONDS); + } catch (final DraytonWiserApiException e) { + logger.warn("Failed to handle command {} for channel {}: {}", command, channelUID, e.getMessage()); + logger.trace("DraytonWiserApiException", e); + } + } + } + } + + private void disposehandleCommandRefreshFuture(final boolean force) { + final ScheduledFuture future = handleCommandRefreshFuture; + + if (future != null) { + future.cancel(force); + } + } + + @Override + public final void dispose() { + disposehandleCommandRefreshFuture(true); + } + + /** + * Performs the actual command. This method is only called when api and device cache are not null. + * + * @param channelId Channel id part of the Channel UID + * @param command the command to perform + * @throws DraytonWiserApiException + */ + protected abstract void handleCommand(String channelId, Command command) throws DraytonWiserApiException; + + @Override + public final void onRefresh(final DraytonWiserDTO draytonWiseDTO) { + this.draytonWiseDTO = draytonWiseDTO; + try { + if (ThingHandlerHelper.isHandlerInitialized(this)) { + data = api == null ? null : collectData(draytonWiseDTO); + refresh(); + if (data == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "No data received"); + } else { + if (getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } + } + } + } catch (final RuntimeException | DraytonWiserApiException e) { + logger.debug("Exception occurred during refresh: {}", e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); + } + } + + /** + * Called to refresh the channels state. + */ + protected abstract void refresh(); + + /** + * Conditionally updates the state. If no data or no api set the state will be set to UNDEF. + * + * @param channelId String id of the channel to update + * @param stateFunction function to return the state, called when api and data are available + */ + protected void updateState(final String channelId, final Supplier stateFunction) { + final State state = api == null || data == null ? UnDefType.UNDEF : stateFunction.get(); + + updateState(channelId, state); + } + + /** + * Returns the handler specific data object only if all data is available. + * If not all data is available it should return null. + * + * @param draytonWiseDTO data object with domain data as received from the hub + * @return handler data object if available else null + * @throws DraytonWiserApiException + */ + protected abstract @Nullable T collectData(DraytonWiserDTO draytonWiseDTO) throws DraytonWiserApiException; + + protected DraytonWiserApi getApi() { + final DraytonWiserApi api = this.api; + + if (api == null) { + throw new IllegalStateException("API not set"); + } + return api; + } + + protected T getData() { + final @Nullable T data = this.data; + + if (data == null) { + throw new IllegalStateException("Data not set"); + } + return data; + } + + protected DraytonWiserDTO getDraytonWiseDTO() { + final DraytonWiserDTO draytonWiseDTO = this.draytonWiseDTO; + + if (draytonWiseDTO == null) { + throw new IllegalStateException("DraytonWiseDTO not set"); + } + return draytonWiseDTO; + } + + @Override + public void bridgeStatusChanged(final ThingStatusInfo bridgeStatusInfo) { + if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) { + if (getThing().getStatus() != ThingStatus.ONLINE) { + final HeatHubHandler bridgeHandler = getHeatHubHandler(); + + api = bridgeHandler == null ? null : bridgeHandler.getApi(); + updateStatus(ThingStatus.UNKNOWN); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } + } + + private @Nullable HeatHubHandler getHeatHubHandler() { + final Bridge bridge = getBridge(); + + if (bridge == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + return null; + } else { + return (HeatHubHandler) bridge.getHandler(); + } + } +} diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/HeatHubConfiguration.java b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/HeatHubConfiguration.java new file mode 100644 index 0000000000000..f2f2d20e29677 --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/HeatHubConfiguration.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2020 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.draytonwiser.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Hilbrand Bouwkamp - Initial contribution + */ +@NonNullByDefault +public class HeatHubConfiguration { + public String networkAddress = ""; + public String secret = ""; + public int refresh; + public int awaySetPoint; +} diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/HeatHubHandler.java b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/HeatHubHandler.java new file mode 100644 index 0000000000000..95d12abec0d6f --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/HeatHubHandler.java @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2010-2020 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.draytonwiser.internal.handler; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +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.eclipse.smarthome.core.cache.ExpiringCache; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.eclipse.smarthome.core.thing.util.ThingHandlerHelper; +import org.eclipse.smarthome.core.types.Command; +import org.openhab.binding.draytonwiser.internal.DraytonWiserRefreshListener; +import org.openhab.binding.draytonwiser.internal.api.DraytonWiserApi; +import org.openhab.binding.draytonwiser.internal.api.DraytonWiserApiException; +import org.openhab.binding.draytonwiser.internal.discovery.DraytonWiserDiscoveryService; +import org.openhab.binding.draytonwiser.internal.model.DeviceDTO; +import org.openhab.binding.draytonwiser.internal.model.DomainDTO; +import org.openhab.binding.draytonwiser.internal.model.DraytonWiserDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link HeatHubHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Andrew Schofield - Initial contribution + * @author Hilbrand Bouwkamp - Moved api and helper code to separate classes + */ +@NonNullByDefault +public class HeatHubHandler extends BaseBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(HeatHubHandler.class); + private final DraytonWiserApi api; + private final ExpiringCache refreshCache = new ExpiringCache<>(3, this::actualRefresh); + + private boolean updateProperties; + private @Nullable DraytonWiserRefreshListener discoveryService; + private @Nullable ScheduledFuture refreshJob; + + public HeatHubHandler(final Bridge thing, final HttpClient httpClient) { + super(thing); + api = new DraytonWiserApi(httpClient); + } + + public DraytonWiserApi getApi() { + return api; + } + + @Override + public Collection> getServices() { + return Collections.singleton(DraytonWiserDiscoveryService.class); + } + + public void setDiscoveryService(final DraytonWiserRefreshListener discoveryService) { + this.discoveryService = discoveryService; + refreshCache.invalidateValue(); + refresh(); + } + + public void unsetDiscoveryService() { + discoveryService = null; + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + } + + @Override + public void initialize() { + logger.debug("Initializing Drayton Wiser Heat Hub handler"); + final HeatHubConfiguration configuration = getConfigAs(HeatHubConfiguration.class); + api.setConfiguration(configuration); + updateProperties = true; + refreshJob = scheduler.scheduleWithFixedDelay(this::refresh, 0, configuration.refresh, TimeUnit.SECONDS); + updateStatus(ThingStatus.UNKNOWN); + } + + public void refresh() { + refreshCache.getValue(); + } + + private @Nullable Boolean actualRefresh() { + try { + if (ThingHandlerHelper.isHandlerInitialized(this)) { + logger.debug("Refreshing devices"); + final DomainDTO domain = api.getDomain(); + + if (domain == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "No data received"); + } else { + if (getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } + final DraytonWiserDTO draytonWiseDTO = new DraytonWiserDTO(domain); + + updateProperties(draytonWiseDTO); + notifyListeners(draytonWiseDTO); + } + logger.debug("Finished refreshing devices"); + } + } catch (final RuntimeException | DraytonWiserApiException e) { + logger.debug("Exception occurred during execution: {}", e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); + return null; + } + return Boolean.TRUE; + } + + @Override + public void childHandlerInitialized(final ThingHandler childHandler, final Thing childThing) { + refresh(); + } + + private void updateProperties(final DraytonWiserDTO draytonWiseDTO) { + if (updateProperties) { + final DeviceDTO device = draytonWiseDTO.getExtendedDeviceProperties(0); + + if (device != null) { + final Map properties = editProperties(); + DraytonWiserPropertyHelper.setGeneralDeviceProperties(device, properties); + updateProperties(properties); + updateProperties = false; + } + } + } + + @Override + public void dispose() { + final ScheduledFuture future = refreshJob; + + if (future != null) { + future.cancel(true); + } + } + + private void notifyListeners(final DraytonWiserDTO domain) { + final DraytonWiserRefreshListener discoveryListener = discoveryService; + + if (discoveryListener != null) { + discoveryListener.onRefresh(domain); + } + getThing().getThings().stream().map(Thing::getHandler) + .filter(handler -> handler instanceof DraytonWiserRefreshListener) + .map(DraytonWiserRefreshListener.class::cast).forEach(listener -> listener.onRefresh(domain)); + } +} diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/HotWaterHandler.java b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/HotWaterHandler.java new file mode 100644 index 0000000000000..6679ff8606a5a --- /dev/null +++ b/bundles/org.openhab.binding.draytonwiser/src/main/java/org/openhab/binding/draytonwiser/internal/handler/HotWaterHandler.java @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2010-2020 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.draytonwiser.internal.handler; + +import static org.openhab.binding.draytonwiser.internal.DraytonWiserBindingConstants.*; + +import java.util.List; + +import javax.measure.quantity.Time; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.draytonwiser.internal.api.DraytonWiserApiException; +import org.openhab.binding.draytonwiser.internal.handler.HotWaterHandler.HotWaterData; +import org.openhab.binding.draytonwiser.internal.model.DraytonWiserDTO; +import org.openhab.binding.draytonwiser.internal.model.HotWaterDTO; +import org.openhab.binding.draytonwiser.internal.model.SystemDTO; + +/** + * The {@link HotWaterHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Andrew Schofield - Initial contribution + * @author Hilbrand Bouwkamp - Simplified handler to handle null data + */ +@NonNullByDefault +public class HotWaterHandler extends DraytonWiserThingHandler { + + public HotWaterHandler(final Thing thing) { + super(thing); + } + + @Override + protected void handleCommand(final String channelId, final Command command) throws DraytonWiserApiException { + if (command instanceof OnOffType && CHANNEL_MANUAL_MODE_STATE.equals(channelId)) { + setManualMode(OnOffType.ON.equals(command)); + } else if (command instanceof OnOffType && CHANNEL_HOT_WATER_SETPOINT.equals(channelId)) { + setSetPoint(OnOffType.ON.equals(command)); + } else if (command instanceof DecimalType && CHANNEL_HOT_WATER_BOOST_DURATION.equals(channelId)) { + setBoostDuration(Math.round((Float.parseFloat(command.toString()) * 60))); + } + } + + @Override + protected void refresh() { + updateState(CHANNEL_HOT_WATER_OVERRIDE, this::getHotWaterOverride); + updateState(CHANNEL_HOTWATER_DEMAND_STATE, this::getHotWaterDemandState); + updateState(CHANNEL_MANUAL_MODE_STATE, this::getManualModeState); + updateState(CHANNEL_HOT_WATER_SETPOINT, this::getSetPointState); + updateState(CHANNEL_HOT_WATER_BOOSTED, this::getBoostedState); + updateState(CHANNEL_HOT_WATER_BOOST_REMAINING, this::getBoostRemainingState); + } + + @Override + protected @Nullable HotWaterData collectData(final DraytonWiserDTO domainDTOProxy) { + final SystemDTO system = domainDTOProxy.getSystem(); + final List hotWater = domainDTOProxy.getHotWater(); + + return system == null ? null : new HotWaterData(system, hotWater); + } + + private State getHotWaterOverride() { + return OnOffType.from("ON".equalsIgnoreCase(getData().system.getHotWaterButtonOverrideState())); + } + + private State getHotWaterDemandState() { + final List hotWater = getData().hotWater; + return OnOffType.from(hotWater.size() >= 1 && "ON".equalsIgnoreCase(hotWater.get(0).getHotWaterRelayState())); + } + + private State getManualModeState() { + final List hotWater = getData().hotWater; + return OnOffType.from(hotWater.size() >= 1 && "MANUAL".equalsIgnoreCase(hotWater.get(0).getMode())); + } + + private State getSetPointState() { + final List hotWater = getData().hotWater; + return OnOffType.from(hotWater.size() >= 1 && "ON".equalsIgnoreCase(hotWater.get(0).getWaterHeatingState())); + } + + private void setManualMode(final boolean manualMode) throws DraytonWiserApiException { + getApi().setHotWaterManualMode(manualMode); + } + + private void setSetPoint(final boolean setPointMode) throws DraytonWiserApiException { + getApi().setHotWaterSetPoint(setPointMode ? 1100 : -200); + } + + private void setBoostDuration(final int durationMinutes) throws DraytonWiserApiException { + if (durationMinutes > 0) { + getApi().setHotWaterBoostActive(durationMinutes); + } else { + getApi().setHotWaterBoostInactive(); + } + } + + private State getBoostedState() { + if (getData().hotWater.size() >= 1) { + final HotWaterDTO firstChannel = getData().hotWater.get(0); + + if (firstChannel.getOverrideTimeoutUnixTime() != null + && !"NONE".equalsIgnoreCase(firstChannel.getOverrideType())) { + return OnOffType.ON; + } + } + + updateState(CHANNEL_HOT_WATER_BOOST_DURATION, DecimalType.ZERO); + + return OnOffType.OFF; + } + + private State getBoostRemainingState() { + if (getData().hotWater.size() >= 1) { + final HotWaterDTO firstChannel = getData().hotWater.get(0); + final Integer overrideTimeout = firstChannel.getOverrideTimeoutUnixTime(); + + if (overrideTimeout != null && !"NONE".equalsIgnoreCase(firstChannel.getOverrideType())) { + return new QuantityType