]> git.basschouten.com Git - openhab-addons.git/commitdiff
[plugwiseha] Initial contribution (#9504)
authorlsiepel <leosiepel@gmail.com>
Wed, 19 May 2021 19:53:33 +0000 (21:53 +0200)
committerGitHub <noreply@github.com>
Wed, 19 May 2021 19:53:33 +0000 (21:53 +0200)
Signed-off-by: Leo Siepel <leosiepel@gmail.com>
59 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.plugwiseha/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/README.md [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/PlugwiseHABindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/PlugwiseHAHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHABadRequestException.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHACommunicationException.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHAException.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHAForbiddenException.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHAInvalidHostException.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHANotAuthorizedException.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHATimeoutException.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHAUnauthorizedException.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/PlugwiseHAController.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/PlugwiseHAControllerRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/PlugwiseHAModel.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/converter/DateTimeConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalities.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionality.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityOffsetTemperature.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityRelay.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityThermostat.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityThreshold.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityTimer.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityToggle.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Appliance.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Appliances.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/DomainObjects.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/GatewayEnvironment.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/GatewayInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Location.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Locations.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Log.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Logs.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Module.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Modules.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/PlugwiseBaseModel.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/PlugwiseComparableDate.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/PlugwiseHACollection.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Service.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Services.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ZigBeeNode.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/xml/PlugwiseHAXStream.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/config/PlugwiseHABridgeThingConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/config/PlugwiseHAThingConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/discovery/PlugwiseHADiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/handler/PlugwiseHAApplianceHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/handler/PlugwiseHABaseHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/handler/PlugwiseHABridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/handler/PlugwiseHAZoneHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/resources/OH-INF/thing/channels.xml [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.plugwiseha/src/main/resources/domain_objects.xslt [new file with mode: 0644]
bundles/pom.xml

index 16a0fb25e1b2fa023c1145242e14fe63f5aba235..ce4bd7ea6d42ccb663266cb408d4fd32550cad5b 100644 (file)
 /bundles/org.openhab.binding.playstation/ @FluBBaOfWard
 /bundles/org.openhab.binding.plclogo/ @falkena
 /bundles/org.openhab.binding.plugwise/ @wborn
+/bundles/org.openhab.binding.plugwiseha/ @lsiepel
 /bundles/org.openhab.binding.powermax/ @lolodomo
 /bundles/org.openhab.binding.pulseaudio/ @peuter
 /bundles/org.openhab.binding.pushbullet/ @hakan42
index 57f14d56c82d7fbcf9215e4164ad9cf7058ed31d..be4084107341b0bde2aabd460a8745112b83e6fa 100644 (file)
       <artifactId>org.openhab.binding.plugwise</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.plugwiseha</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.powermax</artifactId>
diff --git a/bundles/org.openhab.binding.plugwiseha/NOTICE b/bundles/org.openhab.binding.plugwiseha/NOTICE
new file mode 100644 (file)
index 0000000..4c20ef4
--- /dev/null
@@ -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/openhab2-addons
diff --git a/bundles/org.openhab.binding.plugwiseha/README.md b/bundles/org.openhab.binding.plugwiseha/README.md
new file mode 100644 (file)
index 0000000..f566e97
--- /dev/null
@@ -0,0 +1,215 @@
+# PlugwiseHA Binding
+
+The Plugwise Home Automation binding adds support to openHAB for the [Plugwise Home Automation ecosystem](https://www.plugwise.com/en_US/adam_zone_control). 
+This system is built around a gateway from Plugwise called the 'Adam' which incorporates a ZigBee controller to manage thermostatic radiator valves, room thermostats, floor heating pumps, et cetera.
+
+Users can manage and control this system either via a web app or a mobile phone app developed by Plugwise. 
+The (web) app allows users to define heating zone's (e.g. rooms) and add radiator valves to those rooms to manage and control their heating irrespective of other rooms.
+
+Using the Plugwise Home Automation binding you can incorporate the management of these devices and heating zones into openHAB.
+The binding uses the same RESTfull API that both the mobile phone app and the web app use.
+
+The binding requires users to have a working Plugwise Home Automation setup consisting of at least 1 gateway device (the 'Adam') and preferably 1 radiator valve as a bare minimum. 
+The 'Adam' (from hereon called the gateway) needs to be accessible from the openHAB instance via a TCP/IP connection.
+
+## Supported Things
+
+| Device Type                                              | Description                                                                                                        | Thing Type           |
+|----------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|----------------------|
+| -                                                        | A Plugwise heating zone configured with at least 1 of the devices below                                            | zone                 |
+| [Adam](https://www.plugwise.com/en_US/products/adam-ha)  | The Plugwise Home Automation Bridge is needed to connect to the Adam boiler gateway                                | gateway              |
+| [Tom](https://www.plugwise.com/en_US/products/tom)       | A Plugwise Home Automation radiator valve                                                                          | appliance_valve      |
+| [Floor](https://www.plugwise.com/en_US/products/floor)   | A Plugwise Home Automation radiator valve specifically used for floor heating                                       | appliance_valve      |
+| [Circle](https://www.plugwise.com/en_US/products/circle) | A power outlet plug that provides energy measurement and switching control of appliances (e.g. floor heating pump) | appliance_pump       |
+| [Lisa](https://www.plugwise.com/en_US/products/lisa)     | A room thermostat (also supports the 'Anna' room thermostat)                                                       | appliance_thermostat |
+| [Boiler]                                                 | A central boiler used for heating and/or domestic hot water                                                        | appliance_boiler     |
+
+
+
+## Discovery
+
+After setting up the Plugwise Home Automation bridge you can start a manual scan to find all devices registered on the gateway. 
+You can also manually add things by entering the corresponding device id as a configuration parameter. 
+The device IDs can be found be enabling TRACE logging in the Karaf console.
+
+## Thing Configuration
+
+You must define a Plugwise Home Automation gateway (Bridge) before defining zones or appliances (Things) for this binding to work.
+
+#### Plugwise Home Automation gateway (Bridge):
+
+| Parameter | Description                                                             | Config   | Default |
+| --------- | ----------------------------------------------------------------------- | -------- | ------- |
+| host      | The IP address or hostname of the Adam HA gateway                       | Required | 'adam'  |
+| username  | The username for the Adam HA gateway                                    | Optional | 'smile' |
+| smileID   | The 8 letter code on the sticker on the back of the Adam boiler gateway | Required | -       |
+| refresh   | The refresh interval in seconds                                         | Optional | 15      |
+
+#### Plugwise Home Automation zone (`zone`):
+
+| Parameter | Description               | Config   | Default |
+| --------- | ------------------------- | -------- | ------- |
+| id        | The unique ID of the zone | Required | -       |
+
+#### Plugwise Home Automation appliance (`appliance_valve`):
+
+| Parameter            | Description                                                                                                        | Config   | Default |
+| -------------------- | ------------------------------------------------------------------------------------------------------------------ | -------- | ------- |
+| id                   | The unique ID of the radiator valve appliance                                                                      | Required | -       |
+| lowBatteryPercentage | Battery charge remaining at which to trigger battery low warning. (*Only applicable for battery operated devices*) | Optional | 15      |
+
+#### Plugwise Home Automation appliance (`appliance_thermostat`):
+
+| Parameter            | Description                                                                                                        | Config   | Default |
+| -------------------- | ------------------------------------------------------------------------------------------------------------------ | -------- | ------- |
+| id                   | The unique ID of the room thermostat appliance                                                                     | Required | -       |
+| lowBatteryPercentage | Battery charge remaining at which to trigger battery low warning. (*Only applicable for battery operated devices*) | Optional | 15      |
+
+
+#### Plugwise Home Automation appliance (`appliance_pump`):
+
+| Parameter | Description                         | Config   | Default |
+| --------- | ----------------------------------- | -------- | ------- |
+| id        | The unique ID of the pump appliance | Required | -       |
+
+#### Plugwise Home Automation boiler (`appliance_boiler`):
+
+| Parameter | Description                 | Config   | Default |
+| --------- | --------------------------- | -------- | ------- |
+| id        | The unique ID of the boiler | Required | -       |
+
+## Channels
+
+| channel              | type               | Read-only? | description                                                                                                                                                                                          |
+|----------------------|--------------------|------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| temperature          | Number:Temperature | Yes        | The temperature of an appliance that supports the thermostat functionality                                                                                                                           |
+| setpointTemperature  | Number:Temperature | No         | The setpoint temperature (read/write) of an appliance that supports the thermostat functionality                                                                                                     |
+| power                | Switch             | No         | Toggle an appliance ON/OFF that supports the relay functionality                                                                                                                                     |
+| lock                 | Switch             | No         | Toggle an appliance lock ON/OFF that supports the relay functionality.(*When the lock is ON the gateway will not automatically control the corresponding relay switch depending on thermostat mode*) |
+| powerUsage           | Number:Power       | Yes        | The current power usage in Watts of an appliance that supports this                                                                                                                                  |
+| batteryLevel         | Number             | Yes        | The current battery level of an appliance that is battery operated                                                                                                                                   |
+| batteryLevelLow      | Switch             | Yes        | Switches ON when the battery level of an appliance that is battery operated drops below a certain threshold                                                                                          |
+| chState              | Switch             | Yes        | The current central heating state of the boiler                                                                                                                                                      |
+| dhwState             | Switch             | Yes        | The current domestic hot water state of the boiler                                                                                                                                                   |
+| waterPressure        | Number:Pressure    | Yes        | The current water pressure of the boiler                                                                                                                                                             |
+| presetScene          | String             | Yes        | The current active scene for the zone                                                                                                                                                                |
+| valvePosition        | Number             | Yes        | The current position of the valve                                                                                                                                                                    |
+| preHeat              | Switch             | Yes        | Toggle the pre heating of a zone ON/OFF                                                                                                                                                              |
+| coolingState         | Switch             | Yes        | The current cooling state of the boiler                                                                                                                                                              |
+| intendedBoilerTemp   | Number:Temperature | Yes        | The intended boiler temperature                                                                                                                                                                      |
+| flameState           | Switch             | Yes        | The flame state of the boiler                                                                                                                                                                        |
+| intendedHeatingState | Switch             | Yes        | The intended heating state of the boiler                                                                                                                                                             |
+| modulationLevel      | Number             | Yes        | The current modulation level of the boiler                                                                                                                                                           |
+| otAppFaultCode       | Number             | Yes        | The Opentherm application fault code of the boiler                                                                                                                                                   |
+| dhwTemperature       | Number:Temperature | Yes        | The current central heating state of the boiler                                                                                                                                                      |
+| otOEMFaultCode       | Number             | Yes        | The Opentherm OEM fault code of the boiler                                                                                                                                                           |
+| boilerTemperature    | Number:Temperature | Yes        | The current temperature of the boiler                                                                                                                                                                |
+| dhwSetpoint          | Number:Temperature | Yes        | The domestic hot water setpoint                                                                                                                                                                      |
+| maxBoilerTemperature | Number:Temperature | Yes        | The maximum temperature of the boiler                                                                                                                                                                |
+| dhwComfortMode       | Switch             | Yes        | The domestic hot water confortmode                                                                                                                                                                   |
+                                                                                                                                                         |
+
+
+## Full Example
+
+**things/plugwiseha.things**
+
+```
+Bridge plugwiseha:gateway:home "Plugwise Home Automation Gateway" [ smileId="abcdefgh" ] {
+       Thing zone living_room_zone "Living room" [ id="$device_id" ]
+       Thing appliance_valve living_room_radiator "Living room radiator valve" [ id="$device_id" ]
+       Thing appliance_thermostat living_room_thermostat "Living room thermostat" [ id="$device_id" ]
+       Thing appliance_pump living_room_pump "Floor heating pump" [ id="$device_id" ]
+       Thing appliance_boiler main_boiler "Main boiler" [ id="$device_id" ]
+}
+```
+
+Replace `$device_id` accordingly.
+
+**items/plugwiseha.items**
+
+```
+Number:Temperature living_room_zone_temperature "Zone temperature" {channel="plugwiseha:zone:home:living_room_zone:temperature"}
+Number:Temperature living_room_zone_temperature_setpoint "Zone temperature setpoint" {channel="plugwiseha:zone:home:living_room_zone:setpointTemperature"}
+Number:Temperature living_room_zone_preset_scene "Zone preset scene" {channel="plugwiseha:zone:home:living_room_zone:presetScene"}
+Switch living_room_zone_preheat "Zone preheat enabled" {channel="plugwiseha:zone:home:living_room_zone:preHeat"}
+
+Number:Temperature living_room_radiator_temperature "Radiator valve temperature" {channel="plugwiseha:appliance_valve:home:living_room_radiator:temperature"}
+Number:Temperature living_room_radiator_temperature_setpoint "Radiator valve temperature setpoint" {channel="plugwiseha:appliance_valve:home:living_room_radiator:setpointTemperature"}
+Number living_room_radiator_valve_position "Radiator valve position" {channel="plugwiseha:appliance_valve:home:living_room_radiator:valvePosition"}
+
+Number:Temperature living_room_thermostat_temperature "Room thermostat temperature" {channel="plugwiseha:appliance_valve:home:living_room_thermostat:temperature"}
+Number:Temperature living_room_thermostat_temperature_setpoint "Room thermostat temperature setpoint" {channel="plugwiseha:appliance_valve:home:living_room_thermostat:setpointTemperature"}
+Number:Temperature living_room_thermostat_temperature_offset "Room thermostat temperature offset" {channel="plugwiseha:appliance_valve:home:living_room_thermostat:offsetTemperature"}
+
+Switch living_room_pump_power "Floor heating pump power" {channel="plugwiseha:appliance_pump:home:living_room_pump:power"}
+Switch living_room_pump_lock "Floor heating pump lock [MAP:(plugwiseha.map):%s]" {channel="plugwiseha:appliance_pump:home:living_room_pump:lock"}
+Number:Power living_room_pump_power_usage "Floor heating pump power [%0.2fW]" {channel="plugwiseha:appliance_pump:home:living_room_pump:powerUsage"}
+
+Number:Pressure        main_boiler_waterpressure "Waterpressure" { channel="plugwiseha:appliance_boiler:home:main_boiler:waterPressure"}
+Switch main_boiler_chState "Heating active" { channel="plugwiseha:appliance_boiler:home:main_boiler:chActive"}
+Switch main_boiler_dhwState "Domestic hot water active" { channel="plugwiseha:appliance_boiler:home:main_boiler:dhwActive"}
+
+Switch main_boiler_coolingState "Cooling state" { channel="plugwiseha:appliance_boiler:home:main_boiler:coolingState"}
+Number:Temperature main_boiler_intendedBoilerTemp "Intended boiler temperature" {channel="plugwiseha:appliance_boiler:home:living_room_thermostat:intendedBoilerTemp"}
+Switch main_boiler_flameState "Flame state" { channel="plugwiseha:appliance_boiler:home:main_boiler:flameState"}
+Switch main_boiler_intendedHeatingState "Intended heating state" { channel="plugwiseha:appliance_boiler:home:main_boiler:intendedHeatingState"}
+Number main_boiler_modulationLevel "Modulation level" {channel="plugwiseha:appliance_boiler:home:living_room_radiator:modulationLevel"}
+Number main_boiler_otAppFaultCode "Opentherm app. faultcode" {channel="plugwiseha:appliance_boiler:home:living_room_radiator:otAppFaultCode"}
+Number:Temperature main_boiler_dhwTemperature "DHW temperature" {channel="plugwiseha:appliance_boiler:home:living_room_thermostat:dhwTemperature"}
+Number main_boiler_otOEMFaultCode "Opentherm OEM faultcode" {channel="plugwiseha:appliance_boiler:home:living_room_radiator:otOEMFaultCode"}
+Number:Temperature main_boiler_boilerTemperature "Boiler temperature" {channel="plugwiseha:appliance_boiler:home:living_room_thermostat:boilerTemperature"}
+Number:Temperature main_boiler_dhwSetpoint "DHW setpoint" {channel="plugwiseha:appliance_boiler:home:living_room_thermostat:dhwSetpoint"}
+Number:Temperature main_boiler_maxBoilerTemperature "Max. boiler temperature" {channel="plugwiseha:appliance_boiler:home:living_room_thermostat:maxBoilerTemperature"}
+Switch main_boiler_dhwComfortMode "DHW comfort mode" { channel="plugwiseha:appliance_boiler:home:main_boiler:dhwComfortMode"}
+```
+
+**transform/plugwiseha.map**
+
+```
+ON=Locked
+OFF=Unlocked
+```
+
+**sitemaps/plugwiseha.sitemap**
+
+```
+sitemap plugwiseha label="PlugwiseHA Binding"
+{
+       Frame {
+               Text item=living_room_zone_temperature
+               Setpoint item=living_room_zone_temperature_setpoint label="Living room [%.1f Â°C]" minValue=5.0 maxValue=25 step=0.5
+               Text item=living_room_zone_presetScene
+               Switch item=living_room_zone_preheat
+
+               Text item=living_room_radiator_temperature
+               Setpoint item=living_room_radiator_temperature_setpoint label="Living room [%.1f Â°C]" minValue=5.0 maxValue=25 step=0.5
+               Text item=living_room_radiator_valve_position
+
+               Text item=living_room_thermostat_temperature
+               Setpoint item=living_room_thermostat_temperature_setpoint label="Living room [%.1f Â°C]" minValue=5.0 maxValue=25 step=0.5
+               Setpoint item=living_room_thermostat_temperature_offset label="Living room offset [%.1f Â°C]" minValue=-5.0 maxValue=5 step=0.5
+
+               Number item=living_room_pump_power_usage
+               Switch item=living_room_pump_power
+               Switch item=living_room_pump_lock
+
+               Number item=main_boiler_waterpressure
+               Switch item=main_boiler_chState
+               Switch item=main_boiler_dhwState
+
+               Switch item=main_boiler_coolingState
+               Number item=main_boiler_intendedBoilerTemp
+               Switch item=main_boiler_flameState
+               Switch item=main_boiler_intendedHeatingState
+               Number item=main_boiler_modulationLevel
+               Number item=main_boiler_otAppFaultCode
+               Number item=main_boiler_dhwTemperature
+               Number item=main_boiler_otOEMFaultCode
+               Number item=main_boiler_boilerTemperature
+               Number item=main_boiler_dhwSetpoint
+               Number item=main_boiler_maxBoilerTemperature
+               Switch item=main_boiler_dhwComfortMode
+       }
+}
+```
diff --git a/bundles/org.openhab.binding.plugwiseha/pom.xml b/bundles/org.openhab.binding.plugwiseha/pom.xml
new file mode 100644 (file)
index 0000000..9c5ed6e
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.plugwiseha</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: PlugwiseHA Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/feature/feature.xml b/bundles/org.openhab.binding.plugwiseha/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..b7d6c84
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.plugwiseha-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-plugwiseha" description="PlugwiseHA Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.plugwiseha/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/PlugwiseHABindingConstants.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/PlugwiseHABindingConstants.java
new file mode 100644 (file)
index 0000000..cd7e9e6
--- /dev/null
@@ -0,0 +1,165 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.type.ChannelTypeUID;
+
+/**
+ * The {@link PlugwiseHABindingConstants} class defines common constants, which
+ * are used across the whole binding.
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ */
+@NonNullByDefault
+public class PlugwiseHABindingConstants {
+
+    public static final String BINDING_ID = "plugwiseha";
+
+    // List of PlugwiseHA services, related urls, information
+
+    public static final String PLUGWISEHA_API_URL = "http://%s";
+    public static final String PLUGWISEHA_API_APPLIANCES_URL = PLUGWISEHA_API_URL + "/core/appliances";
+    public static final String PLUGWISEHA_API_APPLIANCE_URL = PLUGWISEHA_API_URL + "/core/appliances;id=%s";
+    public static final String PLUGWISEHA_API_LOCATIONS_URL = PLUGWISEHA_API_URL + "/core/locations";
+    public static final String PLUGWISEHA_API_LOCATION_URL = PLUGWISEHA_API_URL + "/core/locations;id=%s";
+
+    // Bridge
+    public static final ThingTypeUID THING_TYPE_GATEWAY = new ThingTypeUID(BINDING_ID, "gateway");
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID THING_TYPE_ZONE = new ThingTypeUID(BINDING_ID, "zone");
+    public static final ThingTypeUID THING_TYPE_APPLIANCE_VALVE = new ThingTypeUID(BINDING_ID, "appliance_valve");
+    public static final ThingTypeUID THING_TYPE_APPLIANCE_PUMP = new ThingTypeUID(BINDING_ID, "appliance_pump");
+    public static final ThingTypeUID THING_TYPE_APPLIANCE_THERMOSTAT = new ThingTypeUID(BINDING_ID,
+            "appliance_thermostat");
+    public static final ThingTypeUID THING_TYPE_APPLIANCE_BOILER = new ThingTypeUID(BINDING_ID, "appliance_boiler");
+
+    // List of channel Type UIDs
+    public static final ChannelTypeUID CHANNEL_TYPE_BATTERYLEVEL = new ChannelTypeUID("system:battery-level");
+    public static final ChannelTypeUID CHANNEL_TYPE_BATTERYLEVELLOW = new ChannelTypeUID("system:low-battery");
+
+    // Empty set
+    public static final Set<ThingTypeUID> SUPPORTED_INTERFACE_TYPES_UIDS_EMPTY = Set.of();
+
+    // List of all Gateway configuration properties
+    public static final String GATEWAY_CONFIG_HOST = "host";
+    public static final String GATEWAY_CONFIG_USERNAME = "username";
+    public static final String GATEWAY_CONFIG_SMILEID = "smileId";
+    public static final String GATEWAY_CONFIG_REFRESH = "refresh";
+
+    // List of all Zone configuration properties
+    public static final String ZONE_CONFIG_ID = "id";
+    public static final String ZONE_CONFIG_NAME = "zoneName";
+
+    // List of all Appliance configuration properties
+    public static final String APPLIANCE_CONFIG_ID = "id";
+    public static final String APPLIANCE_CONFIG_NAME = "applianceName";
+    public static final String APPLIANCE_CONFIG_LOWBATTERY = "lowBatteryPercentage";
+
+    // List of all Appliance properties
+    public static final String APPLIANCE_PROPERTY_DESCRIPTION = "description";
+    public static final String APPLIANCE_PROPERTY_TYPE = "type";
+    public static final String APPLIANCE_PROPERTY_FUNCTIONALITIES = "functionalities";
+    public static final String APPLIANCE_PROPERTY_ZB_TYPE = "zigbee type";
+    public static final String APPLIANCE_PROPERTY_ZB_REACHABLE = "zigbee reachable";
+    public static final String APPLIANCE_PROPERTY_ZB_POWERSOURCE = "zigboo power source";
+
+    // List of all Location properties
+    public static final String LOCATION_PROPERTY_DESCRIPTION = "description";
+    public static final String LOCATION_PROPERTY_TYPE = "type";
+    public static final String LOCATION_PROPERTY_FUNCTIONALITIES = "functionalities";
+
+    // List of all Channel IDs
+    public static final String ZONE_SETPOINT_CHANNEL = "setpointTemperature";
+    public static final String ZONE_TEMPERATURE_CHANNEL = "temperature";
+    public static final String ZONE_PRESETSCENE_CHANNEL = "presetScene";
+    public static final String ZONE_PREHEAT_CHANNEL = "preHeat";
+
+    public static final String APPLIANCE_SETPOINT_CHANNEL = "setpointTemperature";
+    public static final String APPLIANCE_TEMPERATURE_CHANNEL = "temperature";
+    public static final String APPLIANCE_BATTERYLEVEL_CHANNEL = "batteryLevel";
+    public static final String APPLIANCE_BATTERYLEVELLOW_CHANNEL = "batteryLevelLow";
+    public static final String APPLIANCE_POWER_USAGE_CHANNEL = "powerUsage";
+    public static final String APPLIANCE_POWER_CHANNEL = "power";
+    public static final String APPLIANCE_LOCK_CHANNEL = "lock";
+    public static final String APPLIANCE_WATERPRESSURE_CHANNEL = "waterPressure";
+    public static final String APPLIANCE_DHWSTATE_CHANNEL = "dhwState";
+    public static final String APPLIANCE_CHSTATE_CHANNEL = "chState";
+    public static final String APPLIANCE_OFFSET_CHANNEL = "offsetTemperature";
+    public static final String APPLIANCE_VALVEPOSITION_CHANNEL = "valvePosition";
+    public static final String APPLIANCE_COOLINGSTATE_CHANNEL = "coolingState";
+    public static final String APPLIANCE_INTENDEDBOILERTEMP_CHANNEL = "intendedBoilerTemp";
+    public static final String APPLIANCE_FLAMESTATE_CHANNEL = "flameState";
+    public static final String APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL = "intendedHeatingState";
+    public static final String APPLIANCE_MODULATIONLEVEL_CHANNEL = "modulationLevel";
+    public static final String APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL = "otAppFaultCode";
+    public static final String APPLIANCE_DHWTEMPERATURE_CHANNEL = "dhwTemperature";
+    public static final String APPLIANCE_OTOEMFAULTCODE_CHANNEL = "otOEMFaultCode";
+    public static final String APPLIANCE_BOILERTEMPERATURE_CHANNEL = "boilerTemperature";
+    public static final String APPLIANCE_DHWSETPOINT_CHANNEL = "dhwSetpoint";
+    public static final String APPLIANCE_MAXBOILERTEMPERATURE_CHANNEL = "maxBoilerTemperature";
+    public static final String APPLIANCE_DHWCOMFORTMODE_CHANNEL = "dhwComfortMode";
+
+    // List of all Appliance Types
+    public static final String APPLIANCE_TYPE_THERMOSTAT = "thermostat";
+    public static final String APPLIANCE_TYPE_GATEWAY = "gateway";
+    public static final String APPLIANCE_TYPE_CENTRALHEATINGPUMP = "central_heating_pump";
+    public static final String APPLIANCE_TYPE_OPENTHERMGATEWAY = "open_therm_gateway";
+    public static final String APPLIANCE_TYPE_ZONETHERMOSTAT = "zone_thermostat";
+    public static final String APPLIANCE_TYPE_HEATERCENTRAL = "heater_central";
+    public static final String APPLIANCE_TYPE_THERMOSTATICRADIATORVALUE = "thermostatic_radiator_valve";
+
+    // List of Plugwise Maesure Units
+    public static final String UNIT_CELSIUS = "C";
+
+    // Supported things
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ZONE,
+            THING_TYPE_APPLIANCE_VALVE, THING_TYPE_APPLIANCE_PUMP, THING_TYPE_APPLIANCE_BOILER);
+
+    // Appliance types known to binding
+    public static final Set<String> KNOWN_APPLIANCE_TYPES = Set.of(APPLIANCE_TYPE_THERMOSTAT, APPLIANCE_TYPE_GATEWAY,
+            APPLIANCE_TYPE_CENTRALHEATINGPUMP, APPLIANCE_TYPE_OPENTHERMGATEWAY, APPLIANCE_TYPE_ZONETHERMOSTAT,
+            APPLIANCE_TYPE_HEATERCENTRAL, APPLIANCE_TYPE_THERMOSTATICRADIATORVALUE);
+
+    public static final Set<String> SUPPORTED_APPLIANCE_TYPES = Set.of(APPLIANCE_TYPE_CENTRALHEATINGPUMP,
+            APPLIANCE_TYPE_THERMOSTATICRADIATORVALUE, APPLIANCE_TYPE_ZONETHERMOSTAT, APPLIANCE_TYPE_HEATERCENTRAL);
+
+    // Supported bridges
+    public static final Set<ThingTypeUID> SUPPORTED_BRIDGE_TYPES_UIDS = Set.of(THING_TYPE_GATEWAY);
+
+    // Getters & Setters
+    public static String getApiUrl(String host) {
+        return String.format(PLUGWISEHA_API_URL, host);
+    }
+
+    public static String getAppliancesUrl(String host) {
+        return String.format(PLUGWISEHA_API_APPLIANCES_URL, host);
+    }
+
+    public static String getApplianceUrl(String host, String applianceId) {
+        return String.format(PLUGWISEHA_API_APPLIANCE_URL, host, applianceId);
+    }
+
+    public static String getLocationsUrl(String host) {
+        return String.format(PLUGWISEHA_API_LOCATIONS_URL, host);
+    }
+
+    public static String getLocationUrl(String host, String locationId) {
+        return String.format(PLUGWISEHA_API_LOCATION_URL, host, locationId);
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/PlugwiseHAHandlerFactory.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/PlugwiseHAHandlerFactory.java
new file mode 100644 (file)
index 0000000..2d8a8d5
--- /dev/null
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.plugwiseha.internal.handler.PlugwiseHAApplianceHandler;
+import org.openhab.binding.plugwiseha.internal.handler.PlugwiseHABridgeHandler;
+import org.openhab.binding.plugwiseha.internal.handler.PlugwiseHAZoneHandler;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link PlugwiseHAHandlerFactory} is responsible for creating things and
+ * thing handlers.
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ * 
+ */
+@NonNullByDefault
+@Component(service = ThingHandlerFactory.class, configurationPid = "binding.plugwiseha")
+public class PlugwiseHAHandlerFactory extends BaseThingHandlerFactory {
+
+    private final HttpClient httpClient;
+
+    // Constructor
+
+    @Activate
+    public PlugwiseHAHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
+        this.httpClient = httpClientFactory.getCommonHttpClient();
+    }
+
+    // Public methods
+
+    /**
+     * Returns whether the handler is able to create a thing or register a thing
+     * handler for the given type.
+     *
+     * @param thingTypeUID the thing type UID
+     * @return true, if the handler supports the thing type, false otherwise
+     */
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return (PlugwiseHABridgeHandler.supportsThingType(thingTypeUID)
+                || PlugwiseHAZoneHandler.supportsThingType(thingTypeUID))
+                || PlugwiseHAApplianceHandler.supportsThingType(thingTypeUID);
+    }
+
+    /**
+     * Creates a thing for given arguments.
+     *
+     * @param thingTypeUID thing type uid (not null)
+     * @param configuration configuration
+     * @param thingUID thing uid, which can be null
+     * @param bridgeUID bridge uid, which can be null
+     * @return created thing
+     */
+    @Override
+    public @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration,
+            @Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) {
+        if (PlugwiseHABridgeHandler.supportsThingType(thingTypeUID)) {
+            return super.createThing(thingTypeUID, configuration, thingUID, null);
+        } else if (PlugwiseHAZoneHandler.supportsThingType(thingTypeUID)) {
+            return super.createThing(thingTypeUID, configuration, thingUID, bridgeUID);
+        } else if (PlugwiseHAApplianceHandler.supportsThingType(thingTypeUID)) {
+            return super.createThing(thingTypeUID, configuration, thingUID, bridgeUID);
+        }
+
+        throw new IllegalArgumentException(
+                "The thing type " + thingTypeUID + " is not supported by the plugwiseha binding.");
+    }
+
+    // Protected and private methods
+
+    /**
+     * Creates a {@link ThingHandler} for the given thing.
+     *
+     * @param thing the thing
+     * @return thing the created handler
+     */
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (PlugwiseHABridgeHandler.supportsThingType(thingTypeUID)) {
+            return new PlugwiseHABridgeHandler((Bridge) thing, this.httpClient);
+        } else if (PlugwiseHAZoneHandler.supportsThingType(thingTypeUID)) {
+            return new PlugwiseHAZoneHandler(thing);
+        } else if (PlugwiseHAApplianceHandler.supportsThingType(thingTypeUID)) {
+            return new PlugwiseHAApplianceHandler(thing);
+        }
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHABadRequestException.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHABadRequestException.java
new file mode 100644 (file)
index 0000000..19a70b2
--- /dev/null
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PlugwiseHABadRequestException} represents a binding specific {@link Exception}.
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ */
+@NonNullByDefault
+public class PlugwiseHABadRequestException extends PlugwiseHAException {
+
+    private static final long serialVersionUID = 1L;
+
+    public PlugwiseHABadRequestException(String message) {
+        super(message);
+    }
+
+    public PlugwiseHABadRequestException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public PlugwiseHABadRequestException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHACommunicationException.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHACommunicationException.java
new file mode 100644 (file)
index 0000000..699f6c0
--- /dev/null
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PlugwiseHACommunicationException} represents a binding specific {@link Exception}.
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ * 
+ */
+
+@NonNullByDefault
+public class PlugwiseHACommunicationException extends PlugwiseHAException {
+
+    private static final long serialVersionUID = 1L;
+
+    public PlugwiseHACommunicationException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHAException.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHAException.java
new file mode 100644 (file)
index 0000000..a1d76e4
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PlugwiseHAException} represents a binding specific {@link Exception}.
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ * 
+ */
+@NonNullByDefault
+public class PlugwiseHAException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public PlugwiseHAException(String message) {
+        super(message);
+    }
+
+    public PlugwiseHAException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public PlugwiseHAException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHAForbiddenException.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHAForbiddenException.java
new file mode 100644 (file)
index 0000000..68b9af1
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PlugwiseHAForbiddenException} signals the controller denied a request due to invalid credentials.
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ * 
+ */
+
+@NonNullByDefault
+public class PlugwiseHAForbiddenException extends PlugwiseHAException {
+
+    private static final long serialVersionUID = 1L;
+
+    public PlugwiseHAForbiddenException(String message) {
+        super(message);
+    }
+
+    public PlugwiseHAForbiddenException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public PlugwiseHAForbiddenException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHAInvalidHostException.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHAInvalidHostException.java
new file mode 100644 (file)
index 0000000..f56fdeb
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PlugwiseHAInvalidHostException} signals there was a problem with the hostname of the controller.
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ * 
+ */
+
+@NonNullByDefault
+public class PlugwiseHAInvalidHostException extends PlugwiseHAException {
+
+    private static final long serialVersionUID = 1L;
+
+    public PlugwiseHAInvalidHostException(String message) {
+        super(message);
+    }
+
+    public PlugwiseHAInvalidHostException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public PlugwiseHAInvalidHostException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHANotAuthorizedException.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHANotAuthorizedException.java
new file mode 100644 (file)
index 0000000..9bd004e
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PlugwiseHANotAuthorizedException} signals the controller denied a request due to invalid credentials.
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ * 
+ */
+@NonNullByDefault
+public class PlugwiseHANotAuthorizedException extends PlugwiseHAException {
+
+    private static final long serialVersionUID = 1L;
+
+    public PlugwiseHANotAuthorizedException(String message) {
+        super(message);
+    }
+
+    public PlugwiseHANotAuthorizedException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public PlugwiseHANotAuthorizedException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHATimeoutException.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHATimeoutException.java
new file mode 100644 (file)
index 0000000..c50dfe1
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PlugwiseHATimeoutException} represents a binding specific {@link Exception}.
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ */
+
+@NonNullByDefault
+public class PlugwiseHATimeoutException extends PlugwiseHAException {
+
+    private static final long serialVersionUID = 1L;
+
+    public PlugwiseHATimeoutException(String message) {
+        super(message);
+    }
+
+    public PlugwiseHATimeoutException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public PlugwiseHATimeoutException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHAUnauthorizedException.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/exception/PlugwiseHAUnauthorizedException.java
new file mode 100644 (file)
index 0000000..56266d5
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PlugwiseHAUnauthorizedException} represents a binding specific {@link Exception}.
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ */
+
+@NonNullByDefault
+public class PlugwiseHAUnauthorizedException extends PlugwiseHAException {
+
+    private static final long serialVersionUID = 1L;
+
+    public PlugwiseHAUnauthorizedException(String message) {
+        super(message);
+    }
+
+    public PlugwiseHAUnauthorizedException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public PlugwiseHAUnauthorizedException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/PlugwiseHAController.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/PlugwiseHAController.java
new file mode 100644 (file)
index 0000000..d3bef86
--- /dev/null
@@ -0,0 +1,450 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAException;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionality;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionalityOffsetTemperature;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionalityRelay;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionalityThermostat;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Appliance;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Appliances;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.DomainObjects;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.GatewayInfo;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Location;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Locations;
+import org.openhab.binding.plugwiseha.internal.api.xml.PlugwiseHAXStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PlugwiseHAController} class provides the interface to the Plugwise
+ * Home Automation API and stores/caches the object model for use by the various
+ * ThingHandlers of this binding.
+ * 
+ * @author B. van Wetten - Initial contribution
+ */
+@NonNullByDefault
+public class PlugwiseHAController {
+
+    // Private member variables/constants
+
+    private static final int MAX_AGE_MINUTES_REFRESH = 10;
+    private static final int MAX_AGE_MINUTES_FULL_REFRESH = 30;
+    private static final DateTimeFormatter FORMAT = DateTimeFormatter.RFC_1123_DATE_TIME; // default Date format that
+                                                                                          // will be used in conversion
+
+    private final Logger logger = LoggerFactory.getLogger(PlugwiseHAController.class);
+
+    private final HttpClient httpClient;
+    private final PlugwiseHAXStream xStream;
+    private final Transformer domainObjectsTransformer;
+
+    private final String host;
+    private final int port;
+    private final String username;
+    private final String smileId;
+
+    private @Nullable ZonedDateTime gatewayUpdateDateTime;
+    private @Nullable ZonedDateTime gatewayFullUpdateDateTime;
+    private @Nullable DomainObjects domainObjects;
+
+    public PlugwiseHAController(HttpClient httpClient, String host, int port, String username, String smileId)
+            throws PlugwiseHAException {
+        this.httpClient = httpClient;
+        this.host = host;
+        this.port = port;
+        this.username = username;
+        this.smileId = smileId;
+
+        this.xStream = new PlugwiseHAXStream();
+
+        ClassLoader localClassLoader = getClass().getClassLoader();
+        if (localClassLoader != null) {
+            this.domainObjectsTransformer = PlugwiseHAController
+                    .setXSLT(new StreamSource(localClassLoader.getResourceAsStream("domain_objects.xslt")));
+        } else {
+            throw new PlugwiseHAException("PlugwiseHAController.domainObjectsTransformer could not be initialized");
+        }
+    }
+
+    // Public methods
+
+    public void start(Runnable callback) throws PlugwiseHAException {
+        refresh();
+        callback.run();
+    }
+
+    public void refresh() throws PlugwiseHAException {
+        synchronized (this) {
+            this.getUpdatedDomainObjects();
+        }
+    }
+
+    // Public API methods
+
+    public GatewayInfo getGatewayInfo() throws PlugwiseHAException {
+        return getGatewayInfo(false);
+    }
+
+    public GatewayInfo getGatewayInfo(Boolean forceRefresh) throws PlugwiseHAException {
+        GatewayInfo gatewayInfo = null;
+        DomainObjects localDomainObjects = this.domainObjects;
+        if (localDomainObjects != null) {
+            gatewayInfo = localDomainObjects.getGatewayInfo();
+        }
+
+        if (!forceRefresh && gatewayInfo != null) {
+            this.logger.debug("Found Plugwise Home Automation gateway");
+            return gatewayInfo;
+        } else {
+            PlugwiseHAControllerRequest<DomainObjects> request;
+
+            request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
+
+            request.setPath("/core/domain_objects");
+            request.addPathParameter("class", "Gateway");
+
+            DomainObjects domainObjects = executeRequest(request);
+            this.gatewayUpdateDateTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
+
+            return mergeDomainObjects(domainObjects).getGatewayInfo();
+        }
+    }
+
+    public Appliances getAppliances(Boolean forceRefresh) throws PlugwiseHAException {
+        Appliances appliances = null;
+        DomainObjects localDomainObjects = this.domainObjects;
+        if (localDomainObjects != null) {
+            appliances = localDomainObjects.getAppliances();
+        }
+
+        if (!forceRefresh && appliances != null) {
+            return appliances;
+        } else {
+            PlugwiseHAControllerRequest<DomainObjects> request;
+
+            request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
+
+            request.setPath("/core/domain_objects");
+            request.addPathParameter("class", "Appliance");
+
+            DomainObjects domainObjects = executeRequest(request);
+            this.gatewayUpdateDateTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
+            int size = 0;
+            if (!(domainObjects.getAppliances() == null)) {
+                size = domainObjects.getAppliances().size();
+            }
+            this.logger.debug("Found {} Plugwise Home Automation appliance(s)", size);
+
+            return mergeDomainObjects(domainObjects).getAppliances();
+        }
+    }
+
+    public @Nullable Appliance getAppliance(String id, Boolean forceRefresh) throws PlugwiseHAException {
+        Appliances appliances = this.getAppliances(forceRefresh);
+        if (!appliances.containsKey(id)) {
+            this.logger.debug("Plugwise Home Automation Appliance with id {} is not known", id);
+            return null;
+        } else {
+            return appliances.get(id);
+        }
+    }
+
+    public Locations getLocations(Boolean forceRefresh) throws PlugwiseHAException {
+        Locations locations = null;
+        DomainObjects localDomainObjects = this.domainObjects;
+        if (localDomainObjects != null) {
+            locations = localDomainObjects.getLocations();
+        }
+
+        if (!forceRefresh && locations != null) {
+            return locations;
+        } else {
+            PlugwiseHAControllerRequest<DomainObjects> request;
+
+            request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
+
+            request.setPath("/core/domain_objects");
+            request.addPathParameter("class", "Location");
+
+            DomainObjects domainObjects = executeRequest(request);
+            this.gatewayUpdateDateTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
+            int size = 0;
+            if (!(domainObjects.getLocations() == null)) {
+                size = domainObjects.getLocations().size();
+            }
+            this.logger.debug("Found {} Plugwise Home Automation Zone(s)", size);
+            return mergeDomainObjects(domainObjects).getLocations();
+        }
+    }
+
+    public @Nullable Location getLocation(String id, Boolean forceRefresh) throws PlugwiseHAException {
+        Locations locations = this.getLocations(forceRefresh);
+
+        if (!locations.containsKey(id)) {
+            this.logger.debug("Plugwise Home Automation Zone with {} is not known", id);
+            return null;
+        } else {
+            return locations.get(id);
+        }
+    }
+
+    public @Nullable DomainObjects getDomainObjects() throws PlugwiseHAException {
+        PlugwiseHAControllerRequest<DomainObjects> request;
+
+        request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
+
+        request.setPath("/core/domain_objects");
+        request.addPathParameter("@locale", "en-US");
+
+        DomainObjects domainObjects = executeRequest(request);
+        this.gatewayUpdateDateTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
+        this.gatewayFullUpdateDateTime = this.gatewayUpdateDateTime;
+
+        return mergeDomainObjects(domainObjects);
+    }
+
+    public @Nullable DomainObjects getUpdatedDomainObjects() throws PlugwiseHAException {
+        ZonedDateTime localGatewayUpdateDateTime = this.gatewayUpdateDateTime;
+        ZonedDateTime localGatewayFullUpdateDateTime = this.gatewayFullUpdateDateTime;
+        if (localGatewayUpdateDateTime == null
+                || localGatewayUpdateDateTime.isBefore(ZonedDateTime.now().minusMinutes(MAX_AGE_MINUTES_REFRESH))) {
+            return getDomainObjects();
+        } else if (localGatewayFullUpdateDateTime == null || localGatewayFullUpdateDateTime
+                .isBefore(ZonedDateTime.now().minusMinutes(MAX_AGE_MINUTES_FULL_REFRESH))) {
+            return getDomainObjects();
+        } else {
+            return getUpdatedDomainObjects(localGatewayUpdateDateTime);
+        }
+    }
+
+    public @Nullable DomainObjects getUpdatedDomainObjects(ZonedDateTime since) throws PlugwiseHAException {
+        return getUpdatedDomainObjects(since.toEpochSecond());
+    }
+
+    public @Nullable DomainObjects getUpdatedDomainObjects(Long since) throws PlugwiseHAException {
+        PlugwiseHAControllerRequest<DomainObjects> request;
+
+        request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
+
+        request.setPath("/core/domain_objects");
+        request.addPathFilter("modified_date", "ge", since);
+        request.addPathFilter("deleted_date", "ge", "0");
+        request.addPathParameter("@memberModifiedDate", since);
+        request.addPathParameter("@locale", "en-US");
+
+        DomainObjects domainObjects = executeRequest(request);
+        this.gatewayUpdateDateTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
+
+        return mergeDomainObjects(domainObjects);
+    }
+
+    public void setLocationThermostat(Location location, Double temperature) throws PlugwiseHAException {
+        PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
+        Optional<ActuatorFunctionality> thermostat = location.getActuatorFunctionalities().getFunctionalityThermostat();
+
+        if (thermostat.isPresent()) {
+            request.setPath("/core/locations");
+
+            request.addPathParameter("id", String.format("%s/thermostat", location.getId()));
+            request.addPathParameter("id", String.format("%s", thermostat.get().getId()));
+            request.setBodyParameter(new ActuatorFunctionalityThermostat(temperature));
+
+            executeRequest(request);
+        }
+    }
+
+    public void setThermostat(Appliance appliance, Double temperature) throws PlugwiseHAException {
+        PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
+        Optional<ActuatorFunctionality> thermostat = appliance.getActuatorFunctionalities()
+                .getFunctionalityThermostat();
+
+        if (thermostat.isPresent()) {
+            request.setPath("/core/appliances");
+
+            request.addPathParameter("id", String.format("%s/thermostat", appliance.getId()));
+            request.addPathParameter("id", String.format("%s", thermostat.get().getId()));
+            request.setBodyParameter(new ActuatorFunctionalityThermostat(temperature));
+
+            executeRequest(request);
+        }
+    }
+
+    public void setOffsetTemperature(Appliance appliance, Double temperature) throws PlugwiseHAException {
+        PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
+        Optional<ActuatorFunctionality> offsetTemperatureFunctionality = appliance.getActuatorFunctionalities()
+                .getFunctionalityOffsetTemperature();
+
+        if (offsetTemperatureFunctionality.isPresent()) {
+            request.setPath("/core/appliances");
+
+            request.addPathParameter("id", String.format("%s/offset", appliance.getId()));
+            request.addPathParameter("id", String.format("%s", offsetTemperatureFunctionality.get().getId()));
+            request.setBodyParameter(new ActuatorFunctionalityOffsetTemperature(temperature));
+
+            executeRequest(request);
+        }
+    }
+
+    public void switchRelay(Appliance appliance, String state) throws PlugwiseHAException {
+        List<String> allowStates = Arrays.asList("on", "off");
+        if (allowStates.contains(state.toLowerCase())) {
+            if (state.toLowerCase().equals("on")) {
+                switchRelayOn(appliance);
+            } else {
+                switchRelayOff(appliance);
+            }
+        }
+    }
+
+    public void setPreHeating(Location location, Boolean state) throws PlugwiseHAException {
+        PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
+        Optional<ActuatorFunctionality> thermostat = location.getActuatorFunctionalities().getFunctionalityThermostat();
+
+        request.setPath("/core/locations");
+        request.addPathParameter("id", String.format("%s/thermostat", location.getId()));
+        request.addPathParameter("id", String.format("%s", thermostat.get().getId()));
+        request.setBodyParameter(new ActuatorFunctionalityThermostat(state));
+
+        executeRequest(request);
+    }
+
+    public void switchRelayOn(Appliance appliance) throws PlugwiseHAException {
+        PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
+
+        request.setPath("/core/appliances");
+        request.addPathParameter("id", String.format("%s/relay", appliance.getId()));
+        request.setBodyParameter(new ActuatorFunctionalityRelay("on"));
+
+        executeRequest(request);
+    }
+
+    public void switchRelayOff(Appliance appliance) throws PlugwiseHAException {
+        PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
+
+        request.setPath("/core/appliances");
+        request.addPathParameter("id", String.format("%s/relay", appliance.getId()));
+        request.setBodyParameter(new ActuatorFunctionalityRelay("off"));
+
+        executeRequest(request);
+    }
+
+    public void switchRelayLock(Appliance appliance, String state) throws PlugwiseHAException {
+        List<String> allowStates = Arrays.asList("on", "off");
+        if (allowStates.contains(state.toLowerCase())) {
+            if (state.toLowerCase().equals("on")) {
+                switchRelayLockOn(appliance);
+            } else {
+                switchRelayLockOff(appliance);
+            }
+        }
+    }
+
+    public void switchRelayLockOff(Appliance appliance) throws PlugwiseHAException {
+        PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
+
+        request.setPath("/core/appliances");
+        request.addPathParameter("id", String.format("%s/relay", appliance.getId()));
+        request.setBodyParameter(new ActuatorFunctionalityRelay(null, false));
+
+        executeRequest(request);
+    }
+
+    public void switchRelayLockOn(Appliance appliance) throws PlugwiseHAException {
+        PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
+
+        request.setPath("/core/appliances");
+        request.addPathParameter("id", String.format("%s/relay", appliance.getId()));
+        request.setBodyParameter(new ActuatorFunctionalityRelay(null, true));
+
+        executeRequest(request);
+    }
+
+    public ZonedDateTime ping() throws PlugwiseHAException {
+        PlugwiseHAControllerRequest<Void> request;
+
+        request = newRequest(Void.class, null);
+
+        request.setPath("/cache/gateways");
+        request.addPathParameter("ping");
+
+        executeRequest(request);
+
+        return ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
+    }
+
+    // Protected and private methods
+
+    private static Transformer setXSLT(StreamSource xsltSource) throws PlugwiseHAException {
+        try {
+            return TransformerFactory.newInstance().newTransformer(xsltSource);
+        } catch (TransformerConfigurationException e) {
+            throw new PlugwiseHAException("Could not create XML transformer", e);
+        }
+    }
+
+    private <T> PlugwiseHAControllerRequest<T> newRequest(Class<T> responseType, @Nullable Transformer transformer) {
+        return new PlugwiseHAControllerRequest<T>(responseType, this.xStream, transformer, this.httpClient, this.host,
+                this.port, this.username, this.smileId);
+    }
+
+    private <T> PlugwiseHAControllerRequest<T> newRequest(Class<T> responseType) {
+        return new PlugwiseHAControllerRequest<T>(responseType, this.xStream, null, this.httpClient, this.host,
+                this.port, this.username, this.smileId);
+    }
+
+    @SuppressWarnings("null")
+    private <T> T executeRequest(PlugwiseHAControllerRequest<T> request) throws PlugwiseHAException {
+        T result;
+        result = request.execute();
+        return result;
+    }
+
+    private DomainObjects mergeDomainObjects(@Nullable DomainObjects updatedDomainObjects) {
+        DomainObjects localDomainObjects = this.domainObjects;
+        if (localDomainObjects == null && updatedDomainObjects != null) {
+            return updatedDomainObjects;
+        } else if (localDomainObjects != null && updatedDomainObjects == null) {
+            return localDomainObjects;
+        } else if (localDomainObjects != null && updatedDomainObjects != null) {
+            Appliances appliances = updatedDomainObjects.getAppliances();
+            Locations locations = updatedDomainObjects.getLocations();
+
+            if (appliances != null) {
+                localDomainObjects.mergeAppliances(appliances);
+            }
+
+            if (locations != null) {
+                localDomainObjects.mergeLocations(locations);
+            }
+            return localDomainObjects;
+        } else {
+            return new DomainObjects();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/PlugwiseHAControllerRequest.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/PlugwiseHAControllerRequest.java
new file mode 100644 (file)
index 0000000..f8d047f
--- /dev/null
@@ -0,0 +1,286 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.net.ConnectException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentProvider;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.MimeTypes;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHABadRequestException;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAException;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAForbiddenException;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHATimeoutException;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAUnauthorizedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.thoughtworks.xstream.XStream;
+
+/**
+ * The {@link PlugwiseHAControllerRequest} class is a utility class to create
+ * API requests to the Plugwise Home Automation controller and to deserialize
+ * incoming XML into the appropriate model objects to be used by the {@link
+ * PlugwiseHAController}.
+ * 
+ * @author B. van Wetten - Initial contribution
+ */
+@NonNullByDefault
+public class PlugwiseHAControllerRequest<T> {
+
+    private static final String CONTENT_TYPE_TEXT_XML = MimeTypes.Type.TEXT_XML_8859_1.toString();
+    private static final long TIMEOUT_SECONDS = 5;
+
+    private final Logger logger = LoggerFactory.getLogger(PlugwiseHAControllerRequest.class);
+    private final XStream xStream;
+    private final HttpClient httpClient;
+    private final String host;
+    private final int port;
+    private final Class<T> resultType;
+    private final @Nullable Transformer transformer;
+
+    private Map<String, String> headers = new HashMap<>();
+    private Map<String, String> queryParameters = new HashMap<>();
+    private @Nullable Object bodyParameter;
+    private String serverDateTime = "";
+    private String path = "/";
+
+    // Constructor
+
+    <X extends XStream> PlugwiseHAControllerRequest(Class<T> resultType, X xStream, @Nullable Transformer transformer,
+            HttpClient httpClient, String host, int port, String username, String password) {
+        this.resultType = resultType;
+        this.xStream = xStream;
+        this.transformer = transformer;
+        this.httpClient = httpClient;
+        this.host = host;
+        this.port = port;
+
+        setHeader(HttpHeader.ACCEPT.toString(), CONTENT_TYPE_TEXT_XML);
+
+        // Create Basic Auth header if username and password are supplied
+        if (!username.isBlank() && !password.isBlank()) {
+            setHeader(HttpHeader.AUTHORIZATION.toString(), "Basic " + Base64.getEncoder()
+                    .encodeToString(String.format("%s:%s", username, password).getBytes(StandardCharsets.UTF_8)));
+        }
+    }
+
+    // Public methods
+
+    public void setPath(String path) {
+        this.setPath(path, (HashMap<String, String>) null);
+    }
+
+    public void setPath(String path, @Nullable HashMap<String, String> pathParameters) {
+        this.path = path;
+
+        if (pathParameters != null) {
+            this.path += pathParameters.entrySet().stream().map(Object::toString).collect(Collectors.joining(";"));
+        }
+    }
+
+    public void setHeader(String key, Object value) {
+        this.headers.put(key, String.valueOf(value));
+    }
+
+    public void addPathParameter(String key) {
+        this.path += String.format(";%s", key);
+    }
+
+    public void addPathParameter(String key, Object value) {
+        this.path += String.format(";%s=%s", key, value);
+    }
+
+    public void addPathFilter(String key, String operator, Object value) {
+        this.path += String.format(";%s:%s:%s", key, operator, value);
+    }
+
+    public void setQueryParameter(String key, Object value) {
+        this.queryParameters.put(key, String.valueOf(value));
+    }
+
+    public void setBodyParameter(Object body) {
+        this.bodyParameter = body;
+    }
+
+    public String getServerDateTime() {
+        return this.serverDateTime;
+    }
+
+    @SuppressWarnings("unchecked")
+    public @Nullable T execute() throws PlugwiseHAException {
+        T result;
+        String xml = getContent();
+
+        if (String.class.equals(resultType)) {
+            if (this.transformer != null) {
+                result = (T) this.transformXML(xml);
+            } else {
+                result = (T) xml;
+            }
+        } else if (!Void.class.equals(resultType)) {
+            if (this.transformer != null) {
+                result = (T) this.xStream.fromXML(this.transformXML(xml));
+            } else {
+                result = (T) this.xStream.fromXML(xml);
+            }
+        } else {
+            return null;
+        }
+
+        return result;
+    }
+
+    // Protected and private methods
+
+    private String transformXML(String xml) throws PlugwiseHAException {
+        StringReader input = new StringReader(xml);
+        StringWriter output = new StringWriter();
+        Transformer localTransformer = this.transformer;
+        if (localTransformer != null) {
+            try {
+                localTransformer.transform(new StreamSource(input), new StreamResult(output));
+            } catch (TransformerException e) {
+                logger.debug("Could not apply XML stylesheet", e);
+                throw new PlugwiseHAException("Could not apply XML stylesheet", e);
+            }
+        } else {
+            throw new PlugwiseHAException("Could not transform XML stylesheet, the transformer is null");
+        }
+
+        return output.toString();
+    }
+
+    private String getContent() throws PlugwiseHAException {
+        String content;
+        ContentResponse response;
+
+        try {
+            response = getContentResponse();
+        } catch (PlugwiseHATimeoutException e) {
+            // Retry
+            response = getContentResponse();
+        }
+
+        int status = response.getStatus();
+        switch (status) {
+            case HttpStatus.OK_200:
+            case HttpStatus.ACCEPTED_202:
+                content = response.getContentAsString();
+                if (logger.isTraceEnabled()) {
+                    logger.trace("<< {} {} \n{}", status, HttpStatus.getMessage(status), content);
+                }
+                break;
+            case HttpStatus.BAD_REQUEST_400:
+                throw new PlugwiseHABadRequestException("Bad request");
+            case HttpStatus.UNAUTHORIZED_401:
+                throw new PlugwiseHAUnauthorizedException("Unauthorized");
+            case HttpStatus.FORBIDDEN_403:
+                throw new PlugwiseHAForbiddenException("Forbidden");
+            default:
+                throw new PlugwiseHAException("Unknown HTTP status code " + status + " returned by the controller");
+        }
+
+        this.serverDateTime = response.getHeaders().get("Date");
+
+        return content;
+    }
+
+    private ContentResponse getContentResponse() throws PlugwiseHAException {
+        Request request = newRequest();
+        ContentResponse response;
+
+        if (logger.isTraceEnabled()) {
+            logger.trace(">> {} {}", request.getMethod(), request.getURI());
+        }
+
+        try {
+            response = request.send();
+        } catch (TimeoutException | InterruptedException e) {
+            throw new PlugwiseHATimeoutException(e);
+        } catch (ExecutionException e) {
+            // Unwrap the cause and try to cleanly handle it
+            Throwable cause = e.getCause();
+            if (cause instanceof UnknownHostException) {
+                // Invalid hostname
+                throw new PlugwiseHAException(cause);
+            } else if (cause instanceof ConnectException) {
+                // Cannot connect
+                throw new PlugwiseHAException(cause);
+            } else if (cause instanceof SocketTimeoutException) {
+                throw new PlugwiseHATimeoutException(cause);
+            } else if (cause == null) {
+                // Unable to unwrap
+                throw new PlugwiseHAException(e);
+            } else {
+                // Catch all
+                throw new PlugwiseHAException(cause);
+            }
+        }
+        return response;
+    }
+
+    private Request newRequest() {
+        HttpMethod method = bodyParameter == null ? HttpMethod.GET : HttpMethod.PUT;
+        HttpURI uri = new HttpURI(HttpScheme.HTTP.asString(), this.host, this.port, this.path);
+        Request request = httpClient.newRequest(uri.toString()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+                .method(method);
+
+        for (Entry<String, String> entry : this.headers.entrySet()) {
+            request.header(entry.getKey(), entry.getValue());
+        }
+
+        for (Entry<String, String> entry : this.queryParameters.entrySet()) {
+            request.param(entry.getKey(), entry.getValue());
+        }
+
+        if (this.bodyParameter != null) {
+            String xmlBody = getRequestBodyAsXml();
+            ContentProvider content = new StringContentProvider(CONTENT_TYPE_TEXT_XML, xmlBody, StandardCharsets.UTF_8);
+            request = request.content(content);
+        }
+        return request;
+    }
+
+    private String getRequestBodyAsXml() {
+        return this.xStream.toXML(this.bodyParameter);
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/PlugwiseHAModel.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/PlugwiseHAModel.java
new file mode 100644 (file)
index 0000000..50fd784
--- /dev/null
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PlugwiseHAModel} interface describes common
+ * methods that need to be implemented by any object model class.
+ * 
+ * @author B. van Wetten - Initial contribution
+ */
+
+@NonNullByDefault
+public interface PlugwiseHAModel {
+
+    public abstract boolean isBatteryOperated();
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/converter/DateTimeConverter.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/converter/DateTimeConverter.java
new file mode 100644 (file)
index 0000000..0359921
--- /dev/null
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.converter;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.thoughtworks.xstream.converters.basic.AbstractSingleValueConverter;
+
+/**
+ * The {@link DateTimeConverter} provides a SingleValueConverter for use by XStream when converting
+ * XML documents with a zoned date/time field.
+ * 
+ * @author B. van Wetten - Initial contribution
+ */
+
+@NonNullByDefault
+public class DateTimeConverter extends AbstractSingleValueConverter {
+
+    private final Logger logger = LoggerFactory.getLogger(DateTimeConverter.class);
+    private static final DateTimeFormatter FORMAT = DateTimeFormatter.ISO_OFFSET_DATE_TIME; // default Date format that
+
+    @Override
+    public boolean canConvert(@Nullable @SuppressWarnings("rawtypes") Class type) {
+        if (type == null) {
+            return false;
+        }
+        return ZonedDateTime.class.isAssignableFrom(type);
+    }
+
+    @Override
+    public @Nullable ZonedDateTime fromString(@Nullable String str) {
+        if (str == null || str.isBlank()) {
+            return null;
+        }
+
+        try {
+            ZonedDateTime dateTime = ZonedDateTime.parse(str, DateTimeConverter.FORMAT);
+            return dateTime;
+        } catch (DateTimeParseException e) {
+            logger.debug("Invalid datetime format in {}", str);
+            return null;
+        }
+    }
+
+    public String toString(ZonedDateTime dateTime) {
+        return dateTime.format(DateTimeConverter.FORMAT);
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalities.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalities.java
new file mode 100644 (file)
index 0000000..80e55b0
--- /dev/null
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * The {@link ActuatorFunctionalities} class is an object model class that
+ * mirrors the XML structure provided by the Plugwise Home Automation controller
+ * for the collection of actuator functionalities. (e.g. 'offset', 'relay', et
+ * cetera). It extends the {@link CustomCollection} class.
+ * 
+ * @author B. van Wetten - Initial contribution
+ */
+
+public class ActuatorFunctionalities extends PlugwiseHACollection<ActuatorFunctionality> {
+
+    private static final String THERMOSTAT_FUNCTIONALITY = "thermostat";
+    private static final String OFFSETTEMPERATURE_FUNCTIONALITY = "temperature_offset";
+    private static final String RELAY_FUNCTIONALITY = "relay";
+
+    public Optional<Boolean> getRelayLockState() {
+        return this.getFunctionalityRelay().flatMap(ActuatorFunctionality::getRelayLockState)
+                .map(Boolean::parseBoolean);
+    }
+
+    public Optional<Boolean> getPreHeatState() {
+        return this.getFunctionalityThermostat().flatMap(ActuatorFunctionality::getPreHeatState)
+                .map(Boolean::parseBoolean);
+    }
+
+    public Optional<ActuatorFunctionality> getFunctionalityThermostat() {
+        return Optional.ofNullable(this.get(THERMOSTAT_FUNCTIONALITY));
+    }
+
+    public Optional<ActuatorFunctionality> getFunctionalityOffsetTemperature() {
+        return Optional.ofNullable(this.get(OFFSETTEMPERATURE_FUNCTIONALITY));
+    }
+
+    public Optional<ActuatorFunctionality> getFunctionalityRelay() {
+        return Optional.ofNullable(this.get(RELAY_FUNCTIONALITY));
+    }
+
+    @Override
+    public void merge(Map<String, ActuatorFunctionality> actuatorFunctionalities) {
+        if (actuatorFunctionalities != null) {
+            for (ActuatorFunctionality actuatorFunctionality : actuatorFunctionalities.values()) {
+                String type = actuatorFunctionality.getType();
+                ActuatorFunctionality originalActuatorFunctionality = this.get(type);
+
+                Boolean originalIsOlder = false;
+                if (originalActuatorFunctionality != null) {
+                    originalIsOlder = originalActuatorFunctionality.isOlderThan(actuatorFunctionality);
+                }
+
+                if (originalActuatorFunctionality == null || originalIsOlder) {
+                    this.put(type, actuatorFunctionality);
+                }
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionality.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionality.java
new file mode 100644 (file)
index 0000000..2dac9c7
--- /dev/null
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * The {@link ActuatorFunctionality} class is an object model class that mirrors
+ * the XML structure provided by the Plugwise Home Automation controller for the
+ * any actuator functionality. It implements the {@link PlugwiseComparableDate}
+ * interface and extends the abstract class {@link PlugwiseBaseModel}.
+ * 
+ * @author B. van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ */
+@XStreamAlias("actuator_functionality")
+public class ActuatorFunctionality extends PlugwiseBaseModel implements PlugwiseComparableDate<ActuatorFunctionality> {
+
+    private String type;
+    private String duration;
+    private String setpoint;
+    private String resolution;
+    private String lock;
+
+    @XStreamAlias("preheating_allowed")
+    private String preHeat;
+
+    @XStreamAlias("lower_bound")
+    private String lowerBound;
+
+    @XStreamAlias("upper_bound")
+    private String upperBound;
+
+    @XStreamAlias("updated_date")
+    private ZonedDateTime updatedDate;
+
+    public String getType() {
+        return type;
+    }
+
+    public String getDuration() {
+        return duration;
+    }
+
+    public String getSetpoint() {
+        return setpoint;
+    }
+
+    public String getResolution() {
+        return resolution;
+    }
+
+    public String getLowerBound() {
+        return lowerBound;
+    }
+
+    public String getUpperBound() {
+        return upperBound;
+    }
+
+    public ZonedDateTime getUpdatedDate() {
+        return updatedDate;
+    }
+
+    public Optional<String> getPreHeatState() {
+        return Optional.ofNullable(preHeat);
+    }
+
+    public Optional<String> getRelayLockState() {
+        return Optional.ofNullable(lock);
+    }
+
+    @Override
+    public int compareDateWith(ActuatorFunctionality compareTo) {
+        if (compareTo == null) {
+            return -1;
+        }
+        ZonedDateTime compareToDate = compareTo.getModifiedDate();
+        ZonedDateTime compareFromDate = this.getModifiedDate();
+        if (compareFromDate == null) {
+            return -1;
+        } else if (compareToDate == null) {
+            return 1;
+        } else {
+            return compareFromDate.compareTo(compareToDate);
+        }
+    }
+
+    @Override
+    public boolean isNewerThan(ActuatorFunctionality hasModifiedDate) {
+        return compareDateWith(hasModifiedDate) > 0;
+    }
+
+    @Override
+    public boolean isOlderThan(ActuatorFunctionality hasModifiedDate) {
+        return compareDateWith(hasModifiedDate) < 0;
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityOffsetTemperature.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityOffsetTemperature.java
new file mode 100644 (file)
index 0000000..15169c4
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * @author B. van Wetten - Initial contribution
+ */
+@XStreamAlias("offset_functionality")
+public class ActuatorFunctionalityOffsetTemperature extends ActuatorFunctionality {
+
+    @SuppressWarnings("unused")
+    private Double offset;
+
+    public ActuatorFunctionalityOffsetTemperature(Double temperature) {
+        this.offset = temperature;
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityRelay.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityRelay.java
new file mode 100644 (file)
index 0000000..aa0e715
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * @author B. van Wetten - Initial contribution
+ */
+@XStreamAlias("relay_functionality")
+public class ActuatorFunctionalityRelay extends ActuatorFunctionality {
+
+    @SuppressWarnings("unused")
+    private String state;
+    @SuppressWarnings("unused")
+    private Boolean lock;
+
+    public ActuatorFunctionalityRelay(String state) {
+        this.state = state;
+    }
+
+    public ActuatorFunctionalityRelay(String state, Boolean lock) {
+        this.state = state;
+        this.lock = lock;
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityThermostat.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityThermostat.java
new file mode 100644 (file)
index 0000000..638df4f
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * @author B. van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ */
+@XStreamAlias("thermostat_functionality")
+public class ActuatorFunctionalityThermostat extends ActuatorFunctionality {
+
+    @SuppressWarnings("unused")
+    private Double setpoint;
+
+    @SuppressWarnings("unused")
+    @XStreamAlias("preheating_allowed")
+    private Boolean preheatingAllowed;
+
+    public ActuatorFunctionalityThermostat(Double temperature) {
+        this.setpoint = temperature;
+    }
+
+    public ActuatorFunctionalityThermostat(Boolean preheatingAllowed) {
+        this.preheatingAllowed = preheatingAllowed;
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityThreshold.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityThreshold.java
new file mode 100644 (file)
index 0000000..ee10757
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * @author B. van Wetten - Initial contribution
+ */
+@XStreamAlias("threshold_functionality")
+public class ActuatorFunctionalityThreshold extends ActuatorFunctionality {
+
+    public ActuatorFunctionalityThreshold() {
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityTimer.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityTimer.java
new file mode 100644 (file)
index 0000000..822f2e9
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * @author B. van Wetten - Initial contribution
+ */
+@XStreamAlias("timer_functionality")
+public class ActuatorFunctionalityTimer extends ActuatorFunctionality {
+
+    public ActuatorFunctionalityTimer() {
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityToggle.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ActuatorFunctionalityToggle.java
new file mode 100644 (file)
index 0000000..e2e3b6e
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * @author B. van Wetten - Initial contribution
+ */
+@XStreamAlias("toggle_functionality")
+public class ActuatorFunctionalityToggle extends ActuatorFunctionality {
+
+    public ActuatorFunctionalityToggle() {
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Appliance.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Appliance.java
new file mode 100644 (file)
index 0000000..c4a4bdc
--- /dev/null
@@ -0,0 +1,256 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamImplicit;
+
+/**
+ * The {@link Appliance} class is an object model class that
+ * mirrors the XML structure provided by the Plugwise Home Automation
+ * controller for a Plugwise appliance.
+ * It implements the {@link PlugwiseComparableDate} interface and
+ * extends the abstract class {@link PlugwiseBaseModel}.
+ * 
+ * @author B. van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ */
+@XStreamAlias("appliance")
+public class Appliance extends PlugwiseBaseModel implements PlugwiseComparableDate<Appliance> {
+
+    private String name;
+    private String description;
+    private String type;
+    private String location;
+
+    @XStreamAlias("module")
+    private Module module;
+
+    @XStreamAlias("zig_bee_node")
+    private ZigBeeNode zigbeeNode;
+
+    @XStreamImplicit(itemFieldName = "point_log", keyFieldName = "type")
+    private Logs pointLogs;
+
+    @XStreamImplicit(itemFieldName = "actuator_functionality", keyFieldName = "type")
+    private ActuatorFunctionalities actuatorFunctionalities;
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public String getLocation() {
+        return location;
+    }
+
+    public ZigBeeNode getZigbeeNode() {
+        if (zigbeeNode == null) {
+            zigbeeNode = new ZigBeeNode();
+        }
+        return zigbeeNode;
+    }
+
+    public Module getModule() {
+        if (module == null) {
+            module = new Module();
+        }
+        return module;
+    }
+
+    public Logs getPointLogs() {
+        if (pointLogs == null) {
+            pointLogs = new Logs();
+        }
+        return pointLogs;
+    }
+
+    public ActuatorFunctionalities getActuatorFunctionalities() {
+        if (actuatorFunctionalities == null) {
+            actuatorFunctionalities = new ActuatorFunctionalities();
+        }
+        return actuatorFunctionalities;
+    }
+
+    public Optional<Double> getTemperature() {
+        return this.pointLogs.getTemperature();
+    }
+
+    public Optional<String> getTemperatureUnit() {
+        return this.pointLogs.getTemperatureUnit();
+    }
+
+    public Optional<Double> getSetpointTemperature() {
+        return this.pointLogs.getThermostatTemperature();
+    }
+
+    public Optional<String> getSetpointTemperatureUnit() {
+        return this.pointLogs.getThermostatTemperatureUnit();
+    }
+
+    public Optional<Double> getOffsetTemperature() {
+        return this.pointLogs.getOffsetTemperature();
+    }
+
+    public Optional<String> getOffsetTemperatureUnit() {
+        return this.pointLogs.getOffsetTemperatureUnit();
+    }
+
+    public Optional<Boolean> getRelayState() {
+        return this.pointLogs.getRelayState();
+    }
+
+    public Optional<Boolean> getRelayLockState() {
+        return this.actuatorFunctionalities.getRelayLockState();
+    }
+
+    public Optional<Double> getBatteryLevel() {
+        return this.pointLogs.getBatteryLevel();
+    }
+
+    public Optional<Double> getPowerUsage() {
+        return this.pointLogs.getPowerUsage();
+    }
+
+    public Optional<Double> getValvePosition() {
+        return this.pointLogs.getValvePosition();
+    }
+
+    public Optional<Double> getWaterPressure() {
+        return this.pointLogs.getWaterPressure();
+    }
+
+    public Optional<Boolean> getCHState() {
+        return this.pointLogs.getCHState();
+    }
+
+    public Optional<Boolean> getCoolingState() {
+        return this.pointLogs.getCoolingState();
+    }
+
+    public Optional<Double> getIntendedBoilerTemp() {
+        return this.pointLogs.getIntendedBoilerTemp();
+    }
+
+    public Optional<String> getIntendedBoilerTempUnit() {
+        return this.pointLogs.getIntendedBoilerTempUnit();
+    }
+
+    public Optional<Boolean> getFlameState() {
+        return this.pointLogs.getFlameState();
+    }
+
+    public Optional<Boolean> getIntendedHeatingState() {
+        return this.pointLogs.getIntendedHeatingState();
+    }
+
+    public Optional<Double> getModulationLevel() {
+        return this.pointLogs.getModulationLevel();
+    }
+
+    public Optional<Double> getOTAppFaultCode() {
+        return this.pointLogs.getOTAppFaultCode();
+    }
+
+    public Optional<Double> getDHWTemp() {
+        return this.pointLogs.getDHWTemp();
+    }
+
+    public Optional<String> getDHWTempUnit() {
+        return this.pointLogs.getDHWTempUnit();
+    }
+
+    public Optional<Double> getOTOEMFaultcode() {
+        return this.pointLogs.getOTOEMFaultcode();
+    }
+
+    public Optional<Double> getBoilerTemp() {
+        return this.pointLogs.getBoilerTemp();
+    }
+
+    public Optional<String> getBoilerTempUnit() {
+        return this.pointLogs.getBoilerTempUnit();
+    }
+
+    public Optional<Double> getDHTSetpoint() {
+        return this.pointLogs.getDHTSetpoint();
+    }
+
+    public Optional<String> getDHTSetpointUnit() {
+        return this.pointLogs.getDHTSetpointUnit();
+    }
+
+    public Optional<Double> getMaxBoilerTemp() {
+        return this.pointLogs.getMaxBoilerTemp();
+    }
+
+    public Optional<String> getMaxBoilerTempUnit() {
+        return this.pointLogs.getMaxBoilerTempUnit();
+    }
+
+    public Optional<Boolean> getDHWComfortMode() {
+        return this.pointLogs.getDHWComfortMode();
+    }
+
+    public Optional<Boolean> getDHWState() {
+        return this.pointLogs.getDHWState();
+    }
+
+    public boolean isZigbeeDevice() {
+        return (this.zigbeeNode instanceof ZigBeeNode);
+    }
+
+    public boolean isBatteryOperated() {
+        if (this.zigbeeNode instanceof ZigBeeNode) {
+            return this.zigbeeNode.getPowerSource().equals("battery") && this.getBatteryLevel().isPresent();
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int compareDateWith(Appliance compareTo) {
+        if (compareTo == null) {
+            return -1;
+        }
+        ZonedDateTime compareToDate = compareTo.getModifiedDate();
+        ZonedDateTime compareFromDate = this.getModifiedDate();
+        if (compareFromDate == null) {
+            return -1;
+        } else if (compareToDate == null) {
+            return 1;
+        } else {
+            return compareFromDate.compareTo(compareToDate);
+        }
+    }
+
+    @Override
+    public boolean isNewerThan(Appliance hasModifiedDate) {
+        return compareDateWith(hasModifiedDate) > 0;
+    }
+
+    @Override
+    public boolean isOlderThan(Appliance hasModifiedDate) {
+        return compareDateWith(hasModifiedDate) < 0;
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Appliances.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Appliances.java
new file mode 100644 (file)
index 0000000..3ab695d
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import java.util.Map;
+
+/**
+ * The {@link Appliances} class is an object model class that mirrors the XML
+ * structure provided by the Plugwise Home Automation controller for the
+ * collection of appliances. It extends the {@link PlugwiseHACollection} class.
+ * 
+ * @author B. van Wetten - Initial contribution
+ */
+public class Appliances extends PlugwiseHACollection<Appliance> {
+
+    @Override
+    public void merge(Map<String, Appliance> appliancesToMerge) {
+        if (appliancesToMerge != null) {
+            for (Appliance applianceToMerge : appliancesToMerge.values()) {
+                String id = applianceToMerge.getId();
+                Appliance originalAppliance = this.get(id);
+
+                Boolean originalApplianceIsOlder = false;
+                if (originalAppliance != null) {
+                    originalApplianceIsOlder = originalAppliance.isOlderThan(applianceToMerge);
+                }
+
+                if (originalAppliance != null && originalApplianceIsOlder) {
+                    Logs updatedPointLogs = applianceToMerge.getPointLogs();
+                    if (updatedPointLogs != null) {
+                        updatedPointLogs.merge(originalAppliance.getPointLogs());
+                    }
+
+                    ActuatorFunctionalities updatedActuatorFunctionalities = applianceToMerge
+                            .getActuatorFunctionalities();
+                    if (updatedActuatorFunctionalities != null) {
+                        updatedActuatorFunctionalities.merge(originalAppliance.getActuatorFunctionalities());
+                    }
+
+                    this.put(id, applianceToMerge);
+                }
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/DomainObjects.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/DomainObjects.java
new file mode 100644 (file)
index 0000000..e8371ae
--- /dev/null
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamImplicit;
+
+/**
+ * @author B. van Wetten - Initial contribution
+ */
+@XStreamAlias("domain_objects")
+public class DomainObjects {
+
+    @XStreamAlias("gateway")
+    private GatewayInfo gatewayInfo;
+
+    @XStreamImplicit(itemFieldName = "appliance", keyFieldName = "id")
+    private Appliances appliances = new Appliances();
+
+    @XStreamImplicit(itemFieldName = "location", keyFieldName = "id")
+    private Locations locations = new Locations();
+
+    @SuppressWarnings("unused")
+    @XStreamImplicit(itemFieldName = "module", keyFieldName = "id")
+    private Modules modules = new Modules();
+
+    public GatewayInfo getGatewayInfo() {
+        return gatewayInfo;
+    }
+
+    public Appliances getAppliances() {
+        return appliances;
+    }
+
+    public Locations getLocations() {
+        return locations;
+    }
+
+    public Appliances mergeAppliances(Appliances updatedAppliances) {
+        if (updatedAppliances != null) {
+            this.appliances.merge(updatedAppliances);
+        }
+
+        return this.appliances;
+    }
+
+    public Locations mergeLocations(Locations updatedLocations) {
+        if (updatedLocations != null) {
+            this.locations.merge(updatedLocations);
+        }
+
+        return this.locations;
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/GatewayEnvironment.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/GatewayEnvironment.java
new file mode 100644 (file)
index 0000000..1ceb7e9
--- /dev/null
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * @author B. van Wetten - Initial contribution
+ */
+@XStreamAlias("gateway_environment")
+@SuppressWarnings("unused")
+public class GatewayEnvironment extends PlugwiseBaseModel {
+    private String city;
+    private String country;
+    private String currency;
+    private String latitude;
+    private String longitude;
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/GatewayInfo.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/GatewayInfo.java
new file mode 100644 (file)
index 0000000..2190658
--- /dev/null
@@ -0,0 +1,120 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import java.time.ZonedDateTime;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * @author B. van Wetten - Initial contribution
+ */
+@XStreamAlias("gateway")
+public class GatewayInfo extends PlugwiseBaseModel {
+
+    private String name;
+    private String description;
+    private String hostname;
+    private String timezone;
+    private ZonedDateTime time;
+
+    @XStreamAlias("gateway_environment")
+    private GatewayEnvironment gatewayEnvironment;
+
+    @XStreamAlias("vendor_name")
+    private String vendorName;
+
+    @XStreamAlias("vendor_model")
+    private String vendorModel;
+
+    @XStreamAlias("hardware_version")
+    private String hardwareVersion;
+
+    @XStreamAlias("firmware_version")
+    private String firmwareVersion;
+
+    @XStreamAlias("mac_address")
+    private String macAddress;
+
+    @XStreamAlias("lan_ip")
+    private String lanIp;
+
+    @XStreamAlias("wifi_ip")
+    private String wifiIp;
+
+    @XStreamAlias("last_reset_date")
+    private ZonedDateTime lastResetDate;
+
+    @XStreamAlias("last_boot_date")
+    private ZonedDateTime lastBootDate;
+
+    public ZonedDateTime getTime() {
+        return time;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public String getHostname() {
+        return hostname;
+    }
+
+    public String getTimezone() {
+        return timezone;
+    }
+
+    public GatewayEnvironment getGatewayEnvironment() {
+        return gatewayEnvironment;
+    }
+
+    public String getVendorName() {
+        return vendorName;
+    }
+
+    public String getVendorModel() {
+        return vendorModel;
+    }
+
+    public String getHardwareVersion() {
+        return hardwareVersion;
+    }
+
+    public String getFirmwareVersion() {
+        return firmwareVersion;
+    }
+
+    public String getMacAddress() {
+        return macAddress;
+    }
+
+    public String getLanIp() {
+        return lanIp;
+    }
+
+    public String getWifiIp() {
+        return wifiIp;
+    }
+
+    public ZonedDateTime getLastResetDate() {
+        return lastResetDate;
+    }
+
+    public ZonedDateTime getLastBootDate() {
+        return lastBootDate;
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Location.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Location.java
new file mode 100644 (file)
index 0000000..75b2291
--- /dev/null
@@ -0,0 +1,137 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamImplicit;
+
+/**
+ * The {@link Location} class is an object model class that
+ * mirrors the XML structure provided by the Plugwise Home Automation
+ * controller for a Plugwise zone/location.
+ * It implements the {@link PlugwiseComparableDate} interface and
+ * extends the abstract class {@link PlugwiseBaseModel}.
+ * 
+ * @author B. van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ */
+@XStreamAlias("location")
+public class Location extends PlugwiseBaseModel implements PlugwiseComparableDate<Location> {
+
+    private String name;
+    private String description;
+    private String type;
+    private String preset;
+
+    @XStreamImplicit(itemFieldName = "appliance")
+    private List<String> locationAppliances = new ArrayList<String>();
+
+    @XStreamImplicit(itemFieldName = "point_log", keyFieldName = "type")
+    private Logs pointLogs;
+
+    @XStreamImplicit(itemFieldName = "actuator_functionality", keyFieldName = "type")
+    private ActuatorFunctionalities actuatorFunctionalities;
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public String getPreset() {
+        return preset;
+    }
+
+    public List<String> getLocationAppliances() {
+        return locationAppliances;
+    }
+
+    public Logs getPointLogs() {
+        if (pointLogs == null) {
+            pointLogs = new Logs();
+        }
+        return pointLogs;
+    }
+
+    public ActuatorFunctionalities getActuatorFunctionalities() {
+        if (actuatorFunctionalities == null) {
+            actuatorFunctionalities = new ActuatorFunctionalities();
+        }
+        return actuatorFunctionalities;
+    }
+
+    public Optional<Double> getTemperature() {
+        return this.pointLogs.getTemperature();
+    }
+
+    public Optional<String> getTemperatureUnit() {
+        return this.pointLogs.getTemperatureUnit();
+    }
+
+    public Optional<Double> getSetpointTemperature() {
+        return this.pointLogs.getThermostatTemperature();
+    }
+
+    public Optional<String> getSetpointTemperatureUnit() {
+        return this.pointLogs.getThermostatTemperatureUnit();
+    }
+
+    public Optional<Boolean> getPreHeatState() {
+        return this.actuatorFunctionalities.getPreHeatState();
+    }
+
+    public int applianceCount() {
+        if (this.locationAppliances == null) {
+            return 0;
+        } else {
+            return this.locationAppliances.size();
+        }
+    }
+
+    @Override
+    public int compareDateWith(Location compareTo) {
+        if (compareTo == null) {
+            return -1;
+        }
+        ZonedDateTime compareToDate = compareTo.getModifiedDate();
+        ZonedDateTime compareFromDate = this.getModifiedDate();
+        if (compareFromDate == null) {
+            return -1;
+        } else if (compareToDate == null) {
+            return 1;
+        } else {
+            return compareFromDate.compareTo(compareToDate);
+        }
+    }
+
+    @Override
+    public boolean isNewerThan(Location hasModifiedDate) {
+        return compareDateWith(hasModifiedDate) > 0;
+    }
+
+    @Override
+    public boolean isOlderThan(Location hasModifiedDate) {
+        return compareDateWith(hasModifiedDate) < 0;
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Locations.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Locations.java
new file mode 100644 (file)
index 0000000..8858c15
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import java.util.Map;
+
+/**
+ * The {@link Locations} class is an object model class that mirrors the XML
+ * structure provided by the Plugwise Home Automation controller for the
+ * collection of Plugwise locations/zones. It extends the
+ * {@link PlugwiseHACollection} class.
+ * 
+ * @author B. van Wetten - Initial contribution
+ */
+public class Locations extends PlugwiseHACollection<Location> {
+
+    @Override
+    public void merge(Map<String, Location> locations) {
+        if (locations != null) {
+            for (Location location : locations.values()) {
+                String id = location.getId();
+                Location originalLocation = this.get(id);
+
+                Boolean originalLocationIsOlder = false;
+                if (originalLocation != null) {
+                    originalLocationIsOlder = originalLocation.isOlderThan(location);
+                }
+
+                if (originalLocation != null && originalLocationIsOlder) {
+                    Logs updatedPointLogs = location.getPointLogs();
+                    if (updatedPointLogs != null) {
+                        updatedPointLogs.merge(originalLocation.getPointLogs());
+                    }
+
+                    ActuatorFunctionalities updatedActuatorFunctionalities = location.getActuatorFunctionalities();
+                    if (updatedActuatorFunctionalities != null) {
+                        updatedActuatorFunctionalities.merge(originalLocation.getActuatorFunctionalities());
+                    }
+
+                    this.put(id, location);
+                }
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Log.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Log.java
new file mode 100644 (file)
index 0000000..0b87b00
--- /dev/null
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * @author B. van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ */
+@XStreamAlias("point_log")
+public class Log extends PlugwiseBaseModel implements PlugwiseComparableDate<Log> {
+
+    private String type;
+
+    private String unit;
+
+    private String measurement;
+
+    @XStreamAlias("measurement_date")
+    private ZonedDateTime measurementDate;
+
+    @XStreamAlias("updated_date")
+    private ZonedDateTime updatedDate;
+
+    public String getType() {
+        return type;
+    }
+
+    public String getUnit() {
+        return unit;
+    }
+
+    public Optional<String> getMeasurement() {
+        return Optional.ofNullable(measurement);
+    }
+
+    public Optional<Boolean> getMeasurementAsBoolean() {
+        if (measurement != null) {
+            switch (measurement.toLowerCase()) {
+                case "on":
+                    return Optional.of(true);
+                case "off":
+                    return Optional.of(false);
+                default:
+                    return Optional.empty();
+            }
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    public Optional<Double> getMeasurementAsDouble() {
+        try {
+            if (measurement != null) {
+                return Optional.of(Double.parseDouble(measurement));
+            } else {
+                return Optional.empty();
+            }
+        } catch (NumberFormatException e) {
+            return Optional.empty();
+        }
+    }
+
+    public Optional<String> getMeasurementUnit() {
+        return Optional.ofNullable(unit);
+    }
+
+    public ZonedDateTime getMeasurementDate() {
+        return measurementDate;
+    }
+
+    public ZonedDateTime getUpdatedDate() {
+        return updatedDate;
+    }
+
+    @Override
+    public int compareDateWith(Log compareTo) {
+        if (compareTo == null) {
+            return -1;
+        }
+        ZonedDateTime compareToDate = compareTo.getMeasurementDate();
+        ZonedDateTime compareFromDate = this.getMeasurementDate();
+        if (compareFromDate == null) {
+            return -1;
+        } else if (compareToDate == null) {
+            return 1;
+        } else {
+            return compareFromDate.compareTo(compareToDate);
+        }
+    }
+
+    @Override
+    public boolean isNewerThan(Log hasModifiedDate) {
+        return compareDateWith(hasModifiedDate) > 0;
+    }
+
+    @Override
+    public boolean isOlderThan(Log hasModifiedDate) {
+        return compareDateWith(hasModifiedDate) < 0;
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Logs.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Logs.java
new file mode 100644 (file)
index 0000000..2052544
--- /dev/null
@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * The {@link Logs} class is an object model class that
+ * mirrors the XML structure provided by the Plugwise Home Automation
+ * controller for the collection of logs.
+ * It extends the {@link PlugwiseHACollection} class.
+ * 
+ * @author B. van Wetten - Initial contribution
+ */
+public class Logs extends PlugwiseHACollection<Log> {
+
+    private static final String THERMOSTAT = "thermostat";
+    private static final String TEMPERATURE = "temperature";
+    private static final String TEMPERATURE_OFFSET = "temperature_offset";
+    private static final String BATTERY = "battery";
+    private static final String POWER_USAGE = "electricity_consumed";
+    private static final String RELAY = "relay";
+    private static final String DHWSTATE = "domestic_hot_water_state";
+    private static final String COOLINGSTATE = "cooling_state";
+    private static final String INTENDEDBOILERTEMP = "intended_boiler_temperature";
+    private static final String FLAMESTATE = "flame_state";
+    private static final String INTENDEDHEATINGSTATE = "intended_central_heating_state";
+    private static final String MODULATIONLEVEL = "modulation_level";
+    private static final String OTAPPLICATIONFAULTCODE = "open_therm_application_specific_fault_code";
+    private static final String DHWTEMP = "domestic_hot_water_temperature";
+    private static final String OTOEMFAULTCODE = "open_therm_oem_fault_code";
+    private static final String BOILERTEMP = "boiler_temperature";
+    private static final String DHWSETPOINT = "domestic_hot_water_setpoint";
+    private static final String MAXBOILERTEMP = "maximum_boiler_temperature";
+    private static final String DHWCOMFORTMODE = "domestic_hot_water_comfort_mode";
+    private static final String CHSTATE = "central_heating_state";
+    private static final String VALVE_POSITION = "valve_position";
+    private static final String WATER_PRESSURE = "central_heater_water_pressure";
+
+    public Optional<Boolean> getCoolingState() {
+        return this.getLog(COOLINGSTATE).map(logEntry -> logEntry.getMeasurementAsBoolean()).orElse(Optional.empty());
+    }
+
+    public Optional<Double> getIntendedBoilerTemp() {
+        return this.getLog(INTENDEDBOILERTEMP).map(logEntry -> logEntry.getMeasurementAsDouble())
+                .orElse(Optional.empty());
+    }
+
+    public Optional<String> getIntendedBoilerTempUnit() {
+        return this.getLog(INTENDEDBOILERTEMP).map(logEntry -> logEntry.getMeasurementUnit()).orElse(Optional.empty());
+    }
+
+    public Optional<Boolean> getFlameState() {
+        return this.getLog(FLAMESTATE).map(logEntry -> logEntry.getMeasurementAsBoolean()).orElse(Optional.empty());
+    }
+
+    public Optional<Boolean> getIntendedHeatingState() {
+        return this.getLog(INTENDEDHEATINGSTATE).map(logEntry -> logEntry.getMeasurementAsBoolean())
+                .orElse(Optional.empty());
+    }
+
+    public Optional<Double> getModulationLevel() {
+        return this.getLog(MODULATIONLEVEL).map(logEntry -> logEntry.getMeasurementAsDouble()).orElse(Optional.empty());
+    }
+
+    public Optional<Double> getOTAppFaultCode() {
+        return this.getLog(OTAPPLICATIONFAULTCODE).map(logEntry -> logEntry.getMeasurementAsDouble())
+                .orElse(Optional.empty());
+    }
+
+    public Optional<Double> getDHWTemp() {
+        return this.getLog(DHWTEMP).map(logEntry -> logEntry.getMeasurementAsDouble()).orElse(Optional.empty());
+    }
+
+    public Optional<String> getDHWTempUnit() {
+        return this.getLog(DHWTEMP).map(logEntry -> logEntry.getMeasurementUnit()).orElse(Optional.empty());
+    }
+
+    public Optional<Double> getOTOEMFaultcode() {
+        return this.getLog(OTOEMFAULTCODE).map(logEntry -> logEntry.getMeasurementAsDouble()).orElse(Optional.empty());
+    }
+
+    public Optional<Double> getBoilerTemp() {
+        return this.getLog(BOILERTEMP).map(logEntry -> logEntry.getMeasurementAsDouble()).orElse(Optional.empty());
+    }
+
+    public Optional<String> getBoilerTempUnit() {
+        return this.getLog(BOILERTEMP).map(logEntry -> logEntry.getMeasurementUnit()).orElse(Optional.empty());
+    }
+
+    public Optional<Double> getDHTSetpoint() {
+        return this.getLog(DHWSETPOINT).map(logEntry -> logEntry.getMeasurementAsDouble()).orElse(Optional.empty());
+    }
+
+    public Optional<String> getDHTSetpointUnit() {
+        return this.getLog(DHWSETPOINT).map(logEntry -> logEntry.getMeasurementUnit()).orElse(Optional.empty());
+    }
+
+    public Optional<Double> getMaxBoilerTemp() {
+        return this.getLog(MAXBOILERTEMP).map(logEntry -> logEntry.getMeasurementAsDouble()).orElse(Optional.empty());
+    }
+
+    public Optional<String> getMaxBoilerTempUnit() {
+        return this.getLog(MAXBOILERTEMP).map(logEntry -> logEntry.getMeasurementUnit()).orElse(Optional.empty());
+    }
+
+    public Optional<Boolean> getDHWComfortMode() {
+        return this.getLog(DHWCOMFORTMODE).map(logEntry -> logEntry.getMeasurementAsBoolean()).orElse(Optional.empty());
+    }
+
+    public Optional<Double> getTemperature() {
+        return this.getLog(TEMPERATURE).map(logEntry -> logEntry.getMeasurementAsDouble()).orElse(Optional.empty());
+    }
+
+    public Optional<String> getTemperatureUnit() {
+        return this.getLog(TEMPERATURE).map(logEntry -> logEntry.getMeasurementUnit()).orElse(Optional.empty());
+    }
+
+    public Optional<Double> getThermostatTemperature() {
+        return this.getLog(THERMOSTAT).map(logEntry -> logEntry.getMeasurementAsDouble()).orElse(Optional.empty());
+    }
+
+    public Optional<String> getThermostatTemperatureUnit() {
+        return this.getLog(THERMOSTAT).map(logEntry -> logEntry.getMeasurementUnit()).orElse(Optional.empty());
+    }
+
+    public Optional<Double> getOffsetTemperature() {
+        return this.getLog(TEMPERATURE_OFFSET).map(logEntry -> logEntry.getMeasurementAsDouble())
+                .orElse(Optional.empty());
+    }
+
+    public Optional<String> getOffsetTemperatureUnit() {
+        return this.getLog(TEMPERATURE_OFFSET).map(logEntry -> logEntry.getMeasurementUnit()).orElse(Optional.empty());
+    }
+
+    public Optional<Boolean> getRelayState() {
+        return this.getLog(RELAY).map(logEntry -> logEntry.getMeasurementAsBoolean()).orElse(Optional.empty());
+    }
+
+    public Optional<Boolean> getDHWState() {
+        return this.getLog(DHWSTATE).map(logEntry -> logEntry.getMeasurementAsBoolean()).orElse(Optional.empty());
+    }
+
+    public Optional<Boolean> getCHState() {
+        return this.getLog(CHSTATE).map(logEntry -> logEntry.getMeasurementAsBoolean()).orElse(Optional.empty());
+    }
+
+    public Optional<Double> getValvePosition() {
+        return this.getLog(VALVE_POSITION).map(logEntry -> logEntry.getMeasurementAsDouble()).orElse(Optional.empty());
+    }
+
+    public Optional<Double> getWaterPressure() {
+        return this.getLog(WATER_PRESSURE).map(logEntry -> logEntry.getMeasurementAsDouble()).orElse(Optional.empty());
+    }
+
+    public Optional<Double> getBatteryLevel() {
+        return this.getLog(BATTERY).map(logEntry -> logEntry.getMeasurementAsDouble()).orElse(Optional.empty());
+    }
+
+    public Optional<Double> getPowerUsage() {
+        return this.getLog(POWER_USAGE).map(logEntry -> logEntry.getMeasurementAsDouble()).orElse(Optional.empty());
+    }
+
+    public Optional<Log> getLog(String logItem) {
+        return Optional.ofNullable(this.get(logItem));
+    }
+
+    @Override
+    public void merge(Map<String, Log> logsToMerge) {
+        if (logsToMerge != null) {
+            for (Log logToMerge : logsToMerge.values()) {
+                String type = logToMerge.getType();
+                Log originalLog = this.get(type);
+
+                if (originalLog == null || originalLog.isOlderThan(logToMerge)) {
+                    this.put(type, logToMerge);
+                } else {
+                    this.put(type, originalLog);
+                }
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Module.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Module.java
new file mode 100644 (file)
index 0000000..fa4ca6b
--- /dev/null
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import java.time.ZonedDateTime;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamImplicit;
+
+/**
+ * The {@link Module} class is an object model class that
+ * mirrors the XML structure provided by the Plugwise Home Automation
+ * controller for a Plugwise module.
+ * It implements the {@link PlugwiseComparableDate} interface and
+ * extends the abstract class {@link PlugwiseBaseModel}.
+ * 
+ * @author B. van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ */
+@XStreamAlias("module")
+public class Module extends PlugwiseBaseModel implements PlugwiseComparableDate<Module> {
+
+    @SuppressWarnings("unused")
+    @XStreamImplicit(itemFieldName = "service", keyFieldName = "id")
+    private Services services;
+
+    @XStreamAlias("vendor_name")
+    private String vendorName;
+
+    @XStreamAlias("vendor_model")
+    private String vendorModel;
+
+    @XStreamAlias("hardware_version")
+    private String hardwareVersion;
+
+    @XStreamAlias("firmware_version")
+    private String firmwareVersion;
+
+    public String getVendorName() {
+        return vendorName;
+    }
+
+    public String getVendorModel() {
+        return vendorModel;
+    }
+
+    public String getHardwareVersion() {
+        return hardwareVersion;
+    }
+
+    public String getFirmwareVersion() {
+        return firmwareVersion;
+    }
+
+    @Override
+    public int compareDateWith(Module compareTo) {
+        if (compareTo == null) {
+            return -1;
+        }
+        ZonedDateTime compareToDate = compareTo.getModifiedDate();
+        ZonedDateTime compareFromDate = this.getModifiedDate();
+        if (compareFromDate == null) {
+            return -1;
+        } else if (compareToDate == null) {
+            return 1;
+        } else {
+            return compareFromDate.compareTo(compareToDate);
+        }
+    }
+
+    @Override
+    public boolean isNewerThan(Module hasModifiedDate) {
+        return compareDateWith(hasModifiedDate) > 0;
+    }
+
+    @Override
+    public boolean isOlderThan(Module hasModifiedDate) {
+        return compareDateWith(hasModifiedDate) < 0;
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Modules.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Modules.java
new file mode 100644 (file)
index 0000000..29eb21b
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import java.util.Map;
+
+/**
+ * The {@link Modules} class is an object model class that
+ * mirrors the XML structure provided by the Plugwise Home Automation
+ * controller for the collection of modules.
+ * It extends the {@link PlugwiseHACollection} class.
+ * 
+ * @author B. van Wetten - Initial contribution
+ */
+public class Modules extends PlugwiseHACollection<Module> {
+
+    @Override
+    public void merge(Map<String, Module> modules) {
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/PlugwiseBaseModel.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/PlugwiseBaseModel.java
new file mode 100644 (file)
index 0000000..dbf0adb
--- /dev/null
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import java.time.ZonedDateTime;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * The {@link PlugwiseBaseModel} abstract class contains
+ * methods and properties that similar for all object model classes.
+ * 
+ * @author B. van Wetten - Initial contribution
+ */
+public abstract class PlugwiseBaseModel {
+
+    private String id;
+
+    @XStreamAlias("created_date")
+    private ZonedDateTime createdDate;
+
+    @XStreamAlias("modified_date")
+    private ZonedDateTime modifiedDate;
+
+    @XStreamAlias("updated_date")
+    private ZonedDateTime updateDate;
+
+    @XStreamAlias("deleted_date")
+    private ZonedDateTime deletedDate;
+
+    public String getId() {
+        return id;
+    }
+
+    public ZonedDateTime getCreatedDate() {
+        return createdDate;
+    }
+
+    public ZonedDateTime getModifiedDate() {
+        return modifiedDate;
+    }
+
+    public ZonedDateTime getUpdatedDate() {
+        return updateDate;
+    }
+
+    public ZonedDateTime getDeletedDate() {
+        return deletedDate;
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/PlugwiseComparableDate.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/PlugwiseComparableDate.java
new file mode 100644 (file)
index 0000000..19ec253
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+/**
+ * @author B. van Wetten - Initial contribution
+ */
+public interface PlugwiseComparableDate<T extends PlugwiseBaseModel> {
+    public int compareDateWith(T hasModifiedDate);
+
+    public boolean isOlderThan(T hasModifiedDate);
+
+    public boolean isNewerThan(T hasModifiedDate);
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/PlugwiseHACollection.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/PlugwiseHACollection.java
new file mode 100644 (file)
index 0000000..dacebe7
--- /dev/null
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author B. van Wetten - Initial contribution
+ */
+public abstract class PlugwiseHACollection<T> implements Map<String, T> {
+
+    private final Map<String, T> map = new HashMap<>();
+
+    @Override
+    public int size() {
+        return this.map.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return this.map.isEmpty();
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        return this.map.containsKey(key);
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+        return this.map.containsValue(value);
+    }
+
+    @Override
+    public T get(Object key) {
+        return this.map.get(key);
+    }
+
+    @Override
+    public T put(String key, T value) {
+        return this.map.put(key, value);
+    }
+
+    @Override
+    public T remove(Object key) {
+        return this.map.remove(key);
+    }
+
+    @Override
+    public void putAll(Map<? extends String, ? extends T> m) {
+        this.map.putAll(m);
+    }
+
+    @Override
+    public void clear() {
+        this.map.clear();
+    }
+
+    @Override
+    public Set<String> keySet() {
+        return this.map.keySet();
+    }
+
+    @Override
+    public Collection<T> values() {
+        return this.map.values();
+    }
+
+    @Override
+    public Set<Entry<String, T>> entrySet() {
+        return this.map.entrySet();
+    }
+
+    public abstract void merge(Map<String, T> map);
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Service.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Service.java
new file mode 100644 (file)
index 0000000..f6e68b2
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * @author B. van Wetten - Initial contribution
+ */
+@XStreamAlias("service")
+public class Service extends PlugwiseBaseModel {
+
+    @SuppressWarnings("unused")
+    @XStreamAlias("log_type")
+    private String logType;
+
+    @SuppressWarnings("unused")
+    @XStreamAlias("point_log")
+    private String pointLogId;
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Services.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/Services.java
new file mode 100644 (file)
index 0000000..4de9f70
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import java.util.Map;
+
+/**
+ * The {@link Services} class is an object model class that
+ * mirrors the XML structure provided by the Plugwise Home Automation
+ * controller for the collection of module services.
+ * It extends the {@link PlugwiseHACollection} class.
+ * 
+ * @author B. van Wetten - Initial contribution
+ */
+public class Services extends PlugwiseHACollection<Service> {
+
+    @Override
+    public void merge(Map<String, Service> services) {
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ZigBeeNode.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/model/dto/ZigBeeNode.java
new file mode 100644 (file)
index 0000000..33f1aaf
--- /dev/null
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.model.dto;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * The {@link ZigBeeNode} class is an object model class that
+ * mirrors the XML structure provided by the Plugwise Home Automation
+ * controller for a Plugwise ZigBeeNode.
+ * It extends the abstract class {@link PlugwiseBaseModel}.
+ * 
+ * @author B. van Wetten - Initial contribution
+ */
+@XStreamAlias("ZigBeeNode")
+public class ZigBeeNode extends PlugwiseBaseModel {
+
+    private String type;
+    private String reachable;
+
+    @XStreamAlias("power_source")
+    private String powerSource;
+
+    @XStreamAlias("mac_address")
+    private String macAddress;
+
+    public String getType() {
+        return type;
+    }
+
+    public String getReachable() {
+        return reachable;
+    }
+
+    public String getPowerSource() {
+        return powerSource;
+    }
+
+    public String getMacAddress() {
+        return macAddress;
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/xml/PlugwiseHAXStream.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/api/xml/PlugwiseHAXStream.java
new file mode 100644 (file)
index 0000000..e60eefd
--- /dev/null
@@ -0,0 +1,110 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.api.xml;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.plugwiseha.internal.api.model.converter.DateTimeConverter;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionalities;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionality;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionalityOffsetTemperature;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionalityRelay;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionalityThermostat;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionalityThreshold;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionalityTimer;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionalityToggle;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Appliance;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Appliances;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.DomainObjects;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.GatewayEnvironment;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.GatewayInfo;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Location;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Locations;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Log;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Logs;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Module;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Modules;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Service;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Services;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.ZigBeeNode;
+
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.io.xml.StaxDriver;
+import com.thoughtworks.xstream.io.xml.XmlFriendlyNameCoder;
+import com.thoughtworks.xstream.security.NoTypePermission;
+import com.thoughtworks.xstream.security.NullPermission;
+
+/**
+ * The {@link PlugwiseHAXStream} class is a utility class that wraps an XStream
+ * object and provide additional functionality specific to the PlugwiseHA
+ * binding. It automatically load the correct converter classes and processes
+ * the XStream annotions used by the object classes.
+ * 
+ * @author B. van Wetten - Initial contribution
+ */
+
+@NonNullByDefault
+public class PlugwiseHAXStream extends XStream {
+
+    private static XmlFriendlyNameCoder customCoder = new XmlFriendlyNameCoder("_-", "_");
+
+    public PlugwiseHAXStream() {
+        super(new StaxDriver(PlugwiseHAXStream.customCoder));
+
+        initialize();
+    }
+
+    // Protected methods
+
+    @SuppressWarnings("rawtypes")
+    protected void allowClass(Class clz) {
+        this.processAnnotations(clz);
+        this.allowTypeHierarchy(clz);
+    }
+
+    protected void initialize() {
+        // Configure XStream
+        this.ignoreUnknownElements();
+        this.setClassLoader(getClass().getClassLoader());
+
+        // Clear out existing
+        this.addPermission(NoTypePermission.NONE);
+        this.addPermission(NullPermission.NULL);
+
+        // Whitelist classes
+        this.allowClass(GatewayInfo.class);
+        this.allowClass(GatewayEnvironment.class);
+        this.allowClass(Appliances.class);
+        this.allowClass(Appliance.class);
+        this.allowClass(Modules.class);
+        this.allowClass(Module.class);
+        this.allowClass(Locations.class);
+        this.allowClass(Location.class);
+        this.allowClass(Logs.class);
+        this.allowClass(Log.class);
+        this.allowClass(Services.class);
+        this.allowClass(Service.class);
+        this.allowClass(ZigBeeNode.class);
+        this.allowClass(ActuatorFunctionalities.class);
+        this.allowClass(ActuatorFunctionality.class);
+        this.allowClass(ActuatorFunctionalityThermostat.class);
+        this.allowClass(ActuatorFunctionalityOffsetTemperature.class);
+        this.allowClass(ActuatorFunctionalityRelay.class);
+        this.allowClass(ActuatorFunctionalityTimer.class);
+        this.allowClass(ActuatorFunctionalityThreshold.class);
+        this.allowClass(ActuatorFunctionalityToggle.class);
+        this.allowClass(DomainObjects.class);
+
+        // Register custom converters
+        this.registerConverter(new DateTimeConverter());
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/config/PlugwiseHABridgeThingConfig.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/config/PlugwiseHABridgeThingConfig.java
new file mode 100644 (file)
index 0000000..2be9fb5
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PlugwiseHABridgeThingConfig} encapsulates all the configuration options for an instance of the
+ * {@link PlugwiseHABridgeHandler}.
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ */
+@NonNullByDefault
+public class PlugwiseHABridgeThingConfig {
+
+    private String host = "adam";
+
+    private int port = 80;
+
+    private String username = "smile";
+
+    private String smileId = "";
+
+    private int refresh = 15;
+
+    public String getHost() {
+        return host;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public String getsmileId() {
+        return smileId;
+    }
+
+    public int getRefresh() {
+        return refresh;
+    }
+
+    public boolean isValid() {
+        return !host.isBlank() && !username.isBlank() && !smileId.isBlank();
+    }
+
+    @Override
+    public String toString() {
+        return "PlugwiseHABridgeThingConfig{host = " + host + ", port = " + port + ", username = " + username
+                + ", smileId = *****, refresh = " + refresh + "}";
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/config/PlugwiseHAThingConfig.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/config/PlugwiseHAThingConfig.java
new file mode 100644 (file)
index 0000000..fabda3f
--- /dev/null
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PlugwiseHAThingConfig} encapsulates the configuration options for
+ * an instance of the {@link PlugwiseHAApplianceHandler} and the
+ * {@link PlugwiseHAZoneHandler}
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ */
+@NonNullByDefault
+public class PlugwiseHAThingConfig {
+
+    private String id = "";
+
+    private int lowBatteryPercentage = 15;
+
+    // Getters
+
+    public String getId() {
+        return id;
+    }
+
+    public int getLowBatteryPercentage() {
+        return this.lowBatteryPercentage;
+    }
+
+    // Member methods
+
+    public boolean isValid() {
+        return !id.isBlank() && lowBatteryPercentage > 0 && lowBatteryPercentage < 100;
+    }
+
+    @Override
+    public String toString() {
+        return "PlugwiseHAThingConfig{id = " + id + ", lowBatteryPercentage = " + lowBatteryPercentage + "}";
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/discovery/PlugwiseHADiscoveryService.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/discovery/PlugwiseHADiscoveryService.java
new file mode 100644 (file)
index 0000000..f156489
--- /dev/null
@@ -0,0 +1,212 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.discovery;
+
+import static org.openhab.binding.plugwiseha.internal.PlugwiseHABindingConstants.*;
+
+import java.util.HashMap;
+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.openhab.binding.plugwiseha.internal.PlugwiseHABindingConstants;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAException;
+import org.openhab.binding.plugwiseha.internal.api.model.PlugwiseHAController;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Appliance;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.DomainObjects;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Location;
+import org.openhab.binding.plugwiseha.internal.handler.PlugwiseHABridgeHandler;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PlugwiseHADiscoveryService} class is capable of discovering the
+ * available data from the Plugwise Home Automation gateway
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ */
+@NonNullByDefault
+public class PlugwiseHADiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
+
+    private final Logger logger = LoggerFactory.getLogger(PlugwiseHADiscoveryService.class);
+    private static final int TIMEOUT_SECONDS = 5;
+    private static final int REFRESH_SECONDS = 600;
+    private @Nullable PlugwiseHABridgeHandler bridgeHandler;
+    private @Nullable ScheduledFuture<?> discoveryFuture;
+
+    public PlugwiseHADiscoveryService() {
+        super(SUPPORTED_THING_TYPES_UIDS, TIMEOUT_SECONDS, true);
+    }
+
+    @Override
+    protected synchronized void startScan() {
+        try {
+            discoverDomainObjects();
+        } catch (PlugwiseHAException e) {
+            // Ignore silently
+        }
+    }
+
+    @Override
+    protected synchronized void stopScan() {
+        super.stopScan();
+        removeOlderResults(getTimestampOfLastScan());
+    }
+
+    @Override
+    protected void startBackgroundDiscovery() {
+        logger.debug("Start Plugwise Home Automation background discovery");
+
+        ScheduledFuture<?> localDiscoveryFuture = discoveryFuture;
+        if (localDiscoveryFuture == null || localDiscoveryFuture.isCancelled()) {
+            discoveryFuture = scheduler.scheduleWithFixedDelay(this::startScan, 30, REFRESH_SECONDS, TimeUnit.SECONDS);
+        }
+    }
+
+    @Override
+    protected void stopBackgroundDiscovery() {
+        logger.debug("Stopping Plugwise Home Automation background discovery");
+
+        ScheduledFuture<?> localDiscoveryFuture = discoveryFuture;
+        if (localDiscoveryFuture != null) {
+            if (!localDiscoveryFuture.isCancelled()) {
+                localDiscoveryFuture.cancel(true);
+                localDiscoveryFuture = null;
+            }
+        }
+    }
+
+    @Override
+    public void deactivate() {
+        super.deactivate();
+    }
+
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        if (handler instanceof PlugwiseHABridgeHandler) {
+            bridgeHandler = (PlugwiseHABridgeHandler) handler;
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return bridgeHandler;
+    }
+
+    private void discoverDomainObjects() throws PlugwiseHAException {
+        PlugwiseHAController controller = null;
+        PlugwiseHABridgeHandler localBridgeHandler = this.bridgeHandler;
+        if (localBridgeHandler != null) {
+            controller = localBridgeHandler.getController();
+        }
+
+        if (controller != null) {
+            DomainObjects domainObjects = controller.getDomainObjects();
+
+            if (domainObjects != null) {
+                for (Location location : domainObjects.getLocations().values()) {
+                    // Only add locations with at least 1 appliance (this ignores the 'root' (home)
+                    // location which is the parent of all other locations.)
+                    if (location.applianceCount() > 0) {
+                        locationDiscovery(location);
+                    }
+                }
+
+                for (Appliance appliance : domainObjects.getAppliances().values()) {
+                    // Only add appliances that are required/supported for this binding
+                    if (PlugwiseHABindingConstants.SUPPORTED_APPLIANCE_TYPES.contains(appliance.getType())) {
+                        applianceDiscovery(appliance);
+                    }
+                }
+            }
+        }
+    }
+
+    private void applianceDiscovery(Appliance appliance) {
+        String applianceId = appliance.getId();
+        String applianceName = appliance.getName();
+        String applianceType = appliance.getType();
+
+        PlugwiseHABridgeHandler localBridgeHandler = this.bridgeHandler;
+        if (localBridgeHandler != null) {
+            ThingUID bridgeUID = localBridgeHandler.getThing().getUID();
+
+            ThingUID uid;
+
+            Map<String, Object> configProperties = new HashMap<>();
+
+            configProperties.put(APPLIANCE_CONFIG_ID, applianceId);
+
+            switch (applianceType) {
+                case "thermostatic_radiator_valve":
+                    uid = new ThingUID(PlugwiseHABindingConstants.THING_TYPE_APPLIANCE_VALVE, bridgeUID, applianceId);
+                    configProperties.put(APPLIANCE_CONFIG_LOWBATTERY, 15);
+                    break;
+                case "central_heating_pump":
+                    uid = new ThingUID(PlugwiseHABindingConstants.THING_TYPE_APPLIANCE_PUMP, bridgeUID, applianceId);
+                    break;
+                case "heater_central":
+                    uid = new ThingUID(PlugwiseHABindingConstants.THING_TYPE_APPLIANCE_BOILER, bridgeUID, applianceId);
+                    break;
+                case "zone_thermostat":
+                    uid = new ThingUID(PlugwiseHABindingConstants.THING_TYPE_APPLIANCE_THERMOSTAT, bridgeUID,
+                            applianceId);
+                    configProperties.put(APPLIANCE_CONFIG_LOWBATTERY, 15);
+                    break;
+                default:
+                    return;
+            }
+
+            DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
+                    .withLabel(applianceName).withProperties(configProperties)
+                    .withRepresentationProperty(APPLIANCE_CONFIG_ID).build();
+
+            thingDiscovered(discoveryResult);
+
+            logger.debug("Discovered plugwise appliance type '{}' with name '{}' with id {} ({})", applianceType,
+                    applianceName, applianceId, uid);
+        }
+    }
+
+    private void locationDiscovery(Location location) {
+        String locationId = location.getId();
+        String locationName = location.getName();
+
+        PlugwiseHABridgeHandler localBridgeHandler = this.bridgeHandler;
+        if (localBridgeHandler != null) {
+            ThingUID bridgeUID = localBridgeHandler.getThing().getUID();
+            ThingUID uid = new ThingUID(PlugwiseHABindingConstants.THING_TYPE_ZONE, bridgeUID, locationId);
+
+            Map<String, Object> configProperties = new HashMap<>();
+
+            configProperties.put(ZONE_CONFIG_ID, locationId);
+
+            DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
+                    .withLabel(locationName).withRepresentationProperty(ZONE_CONFIG_ID).withProperties(configProperties)
+                    .build();
+
+            thingDiscovered(discoveryResult);
+
+            logger.debug("Discovered plugwise zone '{}' with id {} ({})", locationName, locationId, uid);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/handler/PlugwiseHAApplianceHandler.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/handler/PlugwiseHAApplianceHandler.java
new file mode 100644 (file)
index 0000000..a93d465
--- /dev/null
@@ -0,0 +1,496 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.handler;
+
+import static org.openhab.binding.plugwiseha.internal.PlugwiseHABindingConstants.*;
+import static org.openhab.core.library.unit.MetricPrefix.*;
+import static org.openhab.core.thing.ThingStatus.*;
+import static org.openhab.core.thing.ThingStatusDetail.BRIDGE_OFFLINE;
+import static org.openhab.core.thing.ThingStatusDetail.COMMUNICATION_ERROR;
+import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Dimensionless;
+import javax.measure.quantity.Power;
+import javax.measure.quantity.Pressure;
+import javax.measure.quantity.Temperature;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.plugwiseha.internal.PlugwiseHABindingConstants;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAException;
+import org.openhab.binding.plugwiseha.internal.api.model.PlugwiseHAController;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Appliance;
+import org.openhab.binding.plugwiseha.internal.config.PlugwiseHAThingConfig;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.builder.ChannelBuilder;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.thing.type.ChannelKind;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PlugwiseHAApplianceHandler} class is responsible for handling
+ * commands and status updates for the Plugwise Home Automation appliances.
+ * Extends @{link PlugwiseHABaseHandler}
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ *
+ */
+@NonNullByDefault
+public class PlugwiseHAApplianceHandler extends PlugwiseHABaseHandler<Appliance, PlugwiseHAThingConfig> {
+
+    private @Nullable Appliance appliance;
+    private final Logger logger = LoggerFactory.getLogger(PlugwiseHAApplianceHandler.class);
+
+    // Constructor
+
+    public PlugwiseHAApplianceHandler(Thing thing) {
+        super(thing);
+    }
+
+    public static boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return PlugwiseHABindingConstants.THING_TYPE_APPLIANCE_VALVE.equals(thingTypeUID)
+                || PlugwiseHABindingConstants.THING_TYPE_APPLIANCE_PUMP.equals(thingTypeUID)
+                || PlugwiseHABindingConstants.THING_TYPE_APPLIANCE_BOILER.equals(thingTypeUID)
+                || PlugwiseHABindingConstants.THING_TYPE_APPLIANCE_THERMOSTAT.equals(thingTypeUID);
+    }
+
+    // Overrides
+
+    @Override
+    protected synchronized void initialize(PlugwiseHAThingConfig config, PlugwiseHABridgeHandler bridgeHandler) {
+        if (thing.getStatus() == INITIALIZING) {
+            logger.debug("Initializing Plugwise Home Automation appliance handler with config = {}", config);
+            if (!config.isValid()) {
+                updateStatus(OFFLINE, CONFIGURATION_ERROR,
+                        "Invalid configuration for Plugwise Home Automation appliance handler.");
+                return;
+            }
+
+            try {
+                PlugwiseHAController controller = bridgeHandler.getController();
+                if (controller != null) {
+                    this.appliance = getEntity(controller, true);
+                    Appliance localAppliance = this.appliance;
+                    if (localAppliance != null) {
+                        if (localAppliance.isBatteryOperated()) {
+                            addBatteryChannels();
+                        }
+                        setApplianceProperties();
+                        updateStatus(ONLINE);
+                    } else {
+                        updateStatus(OFFLINE);
+                    }
+                } else {
+                    updateStatus(OFFLINE, BRIDGE_OFFLINE);
+                }
+            } catch (PlugwiseHAException e) {
+                updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getMessage());
+            }
+        }
+    }
+
+    @Override
+    protected @Nullable Appliance getEntity(PlugwiseHAController controller, Boolean forceRefresh)
+            throws PlugwiseHAException {
+        PlugwiseHAThingConfig config = getPlugwiseThingConfig();
+        Appliance appliance = controller.getAppliance(config.getId(), forceRefresh);
+
+        return appliance;
+    }
+
+    @Override
+    protected void handleCommand(Appliance entity, ChannelUID channelUID, Command command) throws PlugwiseHAException {
+        String channelID = channelUID.getIdWithoutGroup();
+
+        PlugwiseHABridgeHandler bridge = this.getPlugwiseHABridge();
+        if (bridge == null) {
+            return;
+        }
+
+        PlugwiseHAController controller = bridge.getController();
+        if (controller == null) {
+            return;
+        }
+
+        switch (channelID) {
+            case APPLIANCE_LOCK_CHANNEL:
+                if (command instanceof OnOffType) {
+                    try {
+                        if (command == OnOffType.ON) {
+                            controller.switchRelayLockOn(entity);
+                        } else {
+                            controller.switchRelayLockOff(entity);
+                        }
+                    } catch (PlugwiseHAException e) {
+                        logger.warn("Unable to switch relay lock {} for appliance '{}'", (State) command,
+                                entity.getName());
+                    }
+                }
+                break;
+            case APPLIANCE_OFFSET_CHANNEL:
+                if (command instanceof QuantityType) {
+                    Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
+                            ? SIUnits.CELSIUS
+                            : ImperialUnits.FAHRENHEIT;
+                    QuantityType<?> state = ((QuantityType<?>) command).toUnit(unit);
+
+                    if (state != null) {
+                        try {
+                            controller.setOffsetTemperature(entity, state.doubleValue());
+                        } catch (PlugwiseHAException e) {
+                            logger.warn("Unable to update setpoint for zone '{}': {} -> {}", entity.getName(),
+                                    entity.getSetpointTemperature().orElse(null), state.doubleValue());
+                        }
+                    }
+                }
+                break;
+            case APPLIANCE_POWER_CHANNEL:
+                if (command instanceof OnOffType) {
+                    try {
+                        if (command == OnOffType.ON) {
+                            controller.switchRelayOn(entity);
+                        } else {
+                            controller.switchRelayOff(entity);
+                        }
+                    } catch (PlugwiseHAException e) {
+                        logger.warn("Unable to switch relay {} for appliance '{}'", (State) command, entity.getName());
+                    }
+                }
+                break;
+            case APPLIANCE_SETPOINT_CHANNEL:
+                if (command instanceof QuantityType) {
+                    Unit<Temperature> unit = entity.getSetpointTemperatureUnit().orElse(UNIT_CELSIUS)
+                            .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
+                    QuantityType<?> state = ((QuantityType<?>) command).toUnit(unit);
+
+                    if (state != null) {
+                        try {
+                            controller.setThermostat(entity, state.doubleValue());
+                        } catch (PlugwiseHAException e) {
+                            logger.warn("Unable to update setpoint for appliance '{}': {} -> {}", entity.getName(),
+                                    entity.getSetpointTemperature().orElse(null), state.doubleValue());
+                        }
+                    }
+                }
+                break;
+            default:
+                logger.warn("Ignoring unsupported command = {} for channel = {}", command, channelUID);
+        }
+    }
+
+    private State getDefaultState(String channelID) {
+        State state = UnDefType.NULL;
+        switch (channelID) {
+            case APPLIANCE_BATTERYLEVEL_CHANNEL:
+            case APPLIANCE_CHSTATE_CHANNEL:
+            case APPLIANCE_DHWSTATE_CHANNEL:
+            case APPLIANCE_COOLINGSTATE_CHANNEL:
+            case APPLIANCE_INTENDEDBOILERTEMP_CHANNEL:
+            case APPLIANCE_FLAMESTATE_CHANNEL:
+            case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
+            case APPLIANCE_MODULATIONLEVEL_CHANNEL:
+            case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
+            case APPLIANCE_DHWTEMPERATURE_CHANNEL:
+            case APPLIANCE_OTOEMFAULTCODE_CHANNEL:
+            case APPLIANCE_BOILERTEMPERATURE_CHANNEL:
+            case APPLIANCE_DHWSETPOINT_CHANNEL:
+            case APPLIANCE_MAXBOILERTEMPERATURE_CHANNEL:
+            case APPLIANCE_DHWCOMFORTMODE_CHANNEL:
+            case APPLIANCE_OFFSET_CHANNEL:
+            case APPLIANCE_POWER_USAGE_CHANNEL:
+            case APPLIANCE_SETPOINT_CHANNEL:
+            case APPLIANCE_TEMPERATURE_CHANNEL:
+            case APPLIANCE_VALVEPOSITION_CHANNEL:
+            case APPLIANCE_WATERPRESSURE_CHANNEL:
+                state = UnDefType.NULL;
+                break;
+            case APPLIANCE_BATTERYLEVELLOW_CHANNEL:
+            case APPLIANCE_LOCK_CHANNEL:
+            case APPLIANCE_POWER_CHANNEL:
+                state = UnDefType.UNDEF;
+                break;
+        }
+        return state;
+    }
+
+    @Override
+    protected void refreshChannel(Appliance entity, ChannelUID channelUID) {
+        String channelID = channelUID.getIdWithoutGroup();
+        State state = getDefaultState(channelID);
+        PlugwiseHAThingConfig config = getPlugwiseThingConfig();
+
+        switch (channelID) {
+            case APPLIANCE_BATTERYLEVEL_CHANNEL: {
+                Double batteryLevel = entity.getBatteryLevel().orElse(null);
+
+                if (batteryLevel != null) {
+                    batteryLevel = batteryLevel * 100;
+                    state = new QuantityType<Dimensionless>(batteryLevel.intValue(), Units.PERCENT);
+                    if (batteryLevel <= config.getLowBatteryPercentage()) {
+                        updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.ON);
+                    } else {
+                        updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.OFF);
+                    }
+                }
+                break;
+            }
+            case APPLIANCE_BATTERYLEVELLOW_CHANNEL: {
+                Double batteryLevel = entity.getBatteryLevel().orElse(null);
+
+                if (batteryLevel != null) {
+                    batteryLevel *= 100;
+                    if (batteryLevel <= config.getLowBatteryPercentage()) {
+                        state = OnOffType.ON;
+                    } else {
+                        state = OnOffType.OFF;
+                    }
+                }
+                break;
+            }
+            case APPLIANCE_CHSTATE_CHANNEL:
+                if (entity.getCHState().isPresent()) {
+                    state = OnOffType.from(entity.getCHState().get());
+                }
+                break;
+            case APPLIANCE_DHWSTATE_CHANNEL:
+                if (entity.getDHWState().isPresent()) {
+                    state = OnOffType.from(entity.getDHWState().get());
+                }
+                break;
+            case APPLIANCE_LOCK_CHANNEL:
+                Boolean relayLockState = entity.getRelayLockState().orElse(null);
+                if (relayLockState != null) {
+                    state = OnOffType.from(relayLockState);
+                }
+                break;
+            case APPLIANCE_OFFSET_CHANNEL:
+                if (entity.getOffsetTemperature().isPresent()) {
+                    Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
+                            ? SIUnits.CELSIUS
+                            : ImperialUnits.FAHRENHEIT;
+                    state = new QuantityType<Temperature>(entity.getOffsetTemperature().get(), unit);
+                }
+                break;
+            case APPLIANCE_POWER_CHANNEL:
+                if (entity.getRelayState().isPresent()) {
+                    state = OnOffType.from(entity.getRelayState().get());
+                }
+                break;
+            case APPLIANCE_POWER_USAGE_CHANNEL:
+                if (entity.getPowerUsage().isPresent()) {
+                    state = new QuantityType<Power>(entity.getPowerUsage().get(), Units.WATT);
+                }
+                break;
+            case APPLIANCE_SETPOINT_CHANNEL:
+                if (entity.getSetpointTemperature().isPresent()) {
+                    Unit<Temperature> unit = entity.getSetpointTemperatureUnit().orElse(UNIT_CELSIUS)
+                            .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
+                    state = new QuantityType<Temperature>(entity.getSetpointTemperature().get(), unit);
+                }
+                break;
+            case APPLIANCE_TEMPERATURE_CHANNEL:
+                if (entity.getTemperature().isPresent()) {
+                    Unit<Temperature> unit = entity.getTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
+                            ? SIUnits.CELSIUS
+                            : ImperialUnits.FAHRENHEIT;
+                    state = new QuantityType<Temperature>(entity.getTemperature().get(), unit);
+                }
+                break;
+            case APPLIANCE_VALVEPOSITION_CHANNEL:
+                if (entity.getValvePosition().isPresent()) {
+                    Double valvePosition = entity.getValvePosition().get() * 100;
+                    state = new QuantityType<Dimensionless>(valvePosition.intValue(), Units.PERCENT);
+                }
+                break;
+            case APPLIANCE_WATERPRESSURE_CHANNEL:
+                if (entity.getWaterPressure().isPresent()) {
+                    Unit<Pressure> unit = HECTO(SIUnits.PASCAL);
+                    state = new QuantityType<Pressure>(entity.getWaterPressure().get(), unit);
+                }
+                break;
+            case APPLIANCE_COOLINGSTATE_CHANNEL:
+                if (entity.getCoolingState().isPresent()) {
+                    state = OnOffType.from(entity.getCoolingState().get());
+                }
+                break;
+            case APPLIANCE_INTENDEDBOILERTEMP_CHANNEL:
+                if (entity.getIntendedBoilerTemp().isPresent()) {
+                    Unit<Temperature> unit = entity.getIntendedBoilerTempUnit().orElse(UNIT_CELSIUS)
+                            .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
+                    state = new QuantityType<Temperature>(entity.getIntendedBoilerTemp().get(), unit);
+                }
+                break;
+            case APPLIANCE_FLAMESTATE_CHANNEL:
+                if (entity.getFlameState().isPresent()) {
+                    state = OnOffType.from(entity.getFlameState().get());
+                }
+                break;
+            case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
+                if (entity.getIntendedHeatingState().isPresent()) {
+                    state = OnOffType.from(entity.getIntendedHeatingState().get());
+                }
+                break;
+            case APPLIANCE_MODULATIONLEVEL_CHANNEL:
+                if (entity.getModulationLevel().isPresent()) {
+                    Double modulationLevel = entity.getModulationLevel().get() * 100;
+                    state = new QuantityType<Dimensionless>(modulationLevel.intValue(), Units.PERCENT);
+                }
+                break;
+            case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
+                if (entity.getOTAppFaultCode().isPresent()) {
+                    state = new QuantityType<Dimensionless>(entity.getOTAppFaultCode().get().intValue(), Units.PERCENT);
+                }
+                break;
+            case APPLIANCE_DHWTEMPERATURE_CHANNEL:
+                if (entity.getDHWTemp().isPresent()) {
+                    Unit<Temperature> unit = entity.getDHWTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
+                            ? SIUnits.CELSIUS
+                            : ImperialUnits.FAHRENHEIT;
+                    state = new QuantityType<Temperature>(entity.getDHWTemp().get(), unit);
+                }
+                break;
+            case APPLIANCE_OTOEMFAULTCODE_CHANNEL:
+                if (entity.getOTOEMFaultcode().isPresent()) {
+                    state = new QuantityType<Dimensionless>(entity.getOTOEMFaultcode().get().intValue(), Units.PERCENT);
+                }
+                break;
+            case APPLIANCE_BOILERTEMPERATURE_CHANNEL:
+                if (entity.getBoilerTemp().isPresent()) {
+                    Unit<Temperature> unit = entity.getBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
+                            ? SIUnits.CELSIUS
+                            : ImperialUnits.FAHRENHEIT;
+                    state = new QuantityType<Temperature>(entity.getBoilerTemp().get(), unit);
+                }
+                break;
+            case APPLIANCE_DHWSETPOINT_CHANNEL:
+                if (entity.getDHTSetpoint().isPresent()) {
+                    Unit<Temperature> unit = entity.getDHTSetpointUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
+                            ? SIUnits.CELSIUS
+                            : ImperialUnits.FAHRENHEIT;
+                    state = new QuantityType<Temperature>(entity.getDHTSetpoint().get(), unit);
+                }
+                break;
+            case APPLIANCE_MAXBOILERTEMPERATURE_CHANNEL:
+                if (entity.getMaxBoilerTemp().isPresent()) {
+                    Unit<Temperature> unit = entity.getMaxBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
+                            ? SIUnits.CELSIUS
+                            : ImperialUnits.FAHRENHEIT;
+                    state = new QuantityType<Temperature>(entity.getMaxBoilerTemp().get(), unit);
+                }
+                break;
+            case APPLIANCE_DHWCOMFORTMODE_CHANNEL:
+                if (entity.getDHWComfortMode().isPresent()) {
+                    state = OnOffType.from(entity.getDHWComfortMode().get());
+                }
+                break;
+            default:
+                break;
+        }
+
+        if (state != UnDefType.NULL) {
+            updateState(channelID, state);
+        }
+    }
+
+    protected synchronized void addBatteryChannels() {
+        logger.debug("Battery operated appliance: {} detected: adding 'Battery level' and 'Battery low level' channels",
+                thing.getLabel());
+
+        ChannelUID channelUIDBatteryLevel = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVEL_CHANNEL);
+        ChannelUID channelUIDBatteryLevelLow = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVELLOW_CHANNEL);
+
+        boolean channelBatteryLevelExists = false;
+        boolean channelBatteryLowExists = false;
+
+        List<Channel> channels = getThing().getChannels();
+        for (Channel channel : channels) {
+            if (channel.getUID().equals(channelUIDBatteryLevel)) {
+                channelBatteryLevelExists = true;
+            } else if (channel.getUID().equals(channelUIDBatteryLevelLow)) {
+                channelBatteryLowExists = true;
+            }
+            if (channelBatteryLevelExists && channelBatteryLowExists) {
+                break;
+            }
+        }
+
+        if (!channelBatteryLevelExists) {
+            ThingBuilder thingBuilder = editThing();
+
+            Channel channelBatteryLevel = ChannelBuilder.create(channelUIDBatteryLevel, "Number")
+                    .withType(CHANNEL_TYPE_BATTERYLEVEL).withKind(ChannelKind.STATE).withLabel("Battery Level")
+                    .withDescription("Represents the battery level as a percentage (0-100%)").build();
+
+            thingBuilder.withChannel(channelBatteryLevel);
+
+            updateThing(thingBuilder.build());
+        }
+
+        if (!channelBatteryLowExists) {
+            ThingBuilder thingBuilder = editThing();
+
+            Channel channelBatteryLow = ChannelBuilder.create(channelUIDBatteryLevelLow, "Switch")
+                    .withType(CHANNEL_TYPE_BATTERYLEVELLOW).withKind(ChannelKind.STATE).withLabel("Battery Low Level")
+                    .withDescription("Switches ON when battery level gets below threshold level").build();
+
+            thingBuilder.withChannel(channelBatteryLow);
+
+            updateThing(thingBuilder.build());
+        }
+    }
+
+    protected void setApplianceProperties() {
+        Map<String, String> properties = editProperties();
+        logger.debug("Setting thing properties to {}", thing.getLabel());
+        Appliance localAppliance = this.appliance;
+        if (localAppliance != null) {
+            properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_DESCRIPTION, localAppliance.getDescription());
+            properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_TYPE, localAppliance.getType());
+            properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_FUNCTIONALITIES,
+                    String.join(", ", localAppliance.getActuatorFunctionalities().keySet()));
+
+            if (localAppliance.isZigbeeDevice()) {
+                properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_TYPE,
+                        localAppliance.getZigbeeNode().getType());
+                properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_REACHABLE,
+                        localAppliance.getZigbeeNode().getReachable());
+                properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_POWERSOURCE,
+                        localAppliance.getZigbeeNode().getPowerSource());
+                properties.put(Thing.PROPERTY_MAC_ADDRESS, localAppliance.getZigbeeNode().getMacAddress());
+            }
+
+            properties.put(Thing.PROPERTY_FIRMWARE_VERSION, localAppliance.getModule().getFirmwareVersion());
+            properties.put(Thing.PROPERTY_HARDWARE_VERSION, localAppliance.getModule().getHardwareVersion());
+            properties.put(Thing.PROPERTY_VENDOR, localAppliance.getModule().getVendorName());
+            properties.put(Thing.PROPERTY_MODEL_ID, localAppliance.getModule().getVendorModel());
+        }
+        updateProperties(properties);
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/handler/PlugwiseHABaseHandler.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/handler/PlugwiseHABaseHandler.java
new file mode 100644 (file)
index 0000000..be47945
--- /dev/null
@@ -0,0 +1,245 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.handler;
+
+import static org.openhab.core.thing.ThingStatus.*;
+
+import java.lang.reflect.ParameterizedType;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAException;
+import org.openhab.binding.plugwiseha.internal.api.model.PlugwiseHAController;
+import org.openhab.binding.plugwiseha.internal.config.PlugwiseHAThingConfig;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PlugwiseHABaseHandler} abstract class provides common methods and
+ * properties for the ThingHandlers of this binding. Extends @{link
+ * BaseThingHandler}
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ *
+ * @param <E> entity - the Plugwise Home Automation entity class used by this
+ *            thing handler
+ * @param <C> config - the Plugwise Home Automation config class used by this
+ *            thing handler
+ */
+
+@NonNullByDefault
+public abstract class PlugwiseHABaseHandler<E, C extends PlugwiseHAThingConfig> extends BaseThingHandler {
+
+    protected static final String STATUS_DESCRIPTION_COMMUNICATION_ERROR = "Error communicating with the Plugwise Home Automation controller";
+
+    protected final Logger logger = LoggerFactory.getLogger(PlugwiseHABaseHandler.class);
+
+    private Class<?> clazz;
+
+    // Constructor
+    @SuppressWarnings("null")
+    public PlugwiseHABaseHandler(Thing thing) {
+        super(thing);
+        clazz = (Class<?>) (((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[1]);
+    }
+
+    // Abstract methods
+
+    /**
+     * Initializes the Plugwise Entity that this class handles.
+     *
+     * @param config the thing configuration
+     * @param bridge the bridge that this thing is part of
+     */
+    protected abstract void initialize(C config, PlugwiseHABridgeHandler bridge);
+
+    /**
+     * Get the Plugwise Entity that belongs to this ThingHandler
+     *
+     * @param controller the controller for this ThingHandler
+     * @param forceRefresh indicated if the entity should be refreshed from the Plugwise API
+     */
+    protected abstract @Nullable E getEntity(PlugwiseHAController controller, Boolean forceRefresh)
+            throws PlugwiseHAException;
+
+    /**
+     * Handles a {@link RefreshType} command for a given channel.
+     *
+     * @param entity the Plugwise Entity
+     * @param channelUID the channel uid the command is for
+     */
+    protected abstract void refreshChannel(E entity, ChannelUID channelUID);
+
+    /**
+     * Handles a command for a given channel.
+     * 
+     * @param entity the Plugwise Entity
+     * @param channelUID the channel uid the command is for
+     * @param command the command
+     */
+    protected abstract void handleCommand(E entity, ChannelUID channelUID, Command command) throws PlugwiseHAException;
+
+    // Overrides
+
+    @Override
+    public void initialize() {
+        C config = getPlugwiseThingConfig();
+
+        if (checkConfig(config)) {
+            Bridge bridge = getBridge();
+            if (bridge == null || bridge.getHandler() == null
+                    || !(bridge.getHandler() instanceof PlugwiseHABridgeHandler)) {
+                updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                        "You must choose a Plugwise Home Automation bridge for this thing.");
+                return;
+            }
+
+            if (bridge.getStatus() == OFFLINE) {
+                updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
+                        "The Plugwise Home Automation bridge is currently offline.");
+            }
+
+            PlugwiseHABridgeHandler bridgeHandler = (PlugwiseHABridgeHandler) bridge.getHandler();
+            if (bridgeHandler != null) {
+                initialize(config, bridgeHandler);
+            }
+        } else {
+            logger.debug("Invalid config for Plugwise Home Automation thing handler with config = {}", config);
+        }
+    }
+
+    @Override
+    public final void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("Handling command = {} for channel = {}", command, channelUID);
+
+        if (getThing().getStatus() == ONLINE) {
+            PlugwiseHAController controller = getController();
+            if (controller != null) {
+                try {
+                    @Nullable
+                    E entity = getEntity(controller, false);
+                    if (entity != null) {
+                        if (this.isLinked(channelUID)) {
+                            if (command instanceof RefreshType) {
+                                refreshChannel(entity, channelUID);
+                            } else {
+                                handleCommand(entity, channelUID, command);
+                            }
+                        }
+                    }
+                } catch (PlugwiseHAException e) {
+                    logger.warn("Unexpected error handling command = {} for channel = {} : {}", command, channelUID,
+                            e.getMessage());
+                }
+            }
+        }
+    }
+
+    // Public member methods
+
+    public @Nullable PlugwiseHABridgeHandler getPlugwiseHABridge() {
+        Bridge bridge = this.getBridge();
+        if (bridge != null) {
+            return (PlugwiseHABridgeHandler) bridge.getHandler();
+        }
+
+        return null;
+    }
+
+    @SuppressWarnings("unchecked")
+    public C getPlugwiseThingConfig() {
+        return (C) getConfigAs(clazz);
+    }
+
+    // Private & protected methods
+
+    private @Nullable PlugwiseHAController getController() {
+        PlugwiseHABridgeHandler bridgeHandler = getPlugwiseHABridge();
+
+        if (bridgeHandler != null) {
+            return bridgeHandler.getController();
+        }
+
+        return null;
+    }
+
+    /**
+     * Checks the configuration for validity, result is reflected in the status of
+     * the Thing
+     */
+    private boolean checkConfig(C config) {
+        if (!config.isValid()) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "Configuration is missing or corrupted");
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    @Override
+    public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+        super.bridgeStatusChanged(bridgeStatusInfo);
+        if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
+            setLinkedChannelsUndef();
+        }
+    }
+
+    private void setLinkedChannelsUndef() {
+        for (Channel channel : getThing().getChannels()) {
+            ChannelUID channelUID = channel.getUID();
+            if (this.isLinked(channelUID)) {
+                updateState(channelUID, UnDefType.UNDEF);
+            }
+        }
+    }
+
+    protected final void refresh() {
+        PlugwiseHABridgeHandler bridgeHandler = getPlugwiseHABridge();
+        if (bridgeHandler != null) {
+            if (bridgeHandler.getThing().getStatusInfo().getStatus() == ThingStatus.ONLINE) {
+                PlugwiseHAController controller = getController();
+                if (controller != null) {
+                    @Nullable
+                    E entity = null;
+                    try {
+                        entity = getEntity(controller, false);
+                    } catch (PlugwiseHAException e) {
+                        updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+                        setLinkedChannelsUndef();
+                    }
+                    if (entity != null) {
+                        for (Channel channel : getThing().getChannels()) {
+                            ChannelUID channelUID = channel.getUID();
+                            if (this.isLinked(channelUID)) {
+                                refreshChannel(entity, channelUID);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/handler/PlugwiseHABridgeHandler.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/handler/PlugwiseHABridgeHandler.java
new file mode 100644 (file)
index 0000000..a2cc48c
--- /dev/null
@@ -0,0 +1,252 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.handler;
+
+import static org.openhab.binding.plugwiseha.internal.PlugwiseHABindingConstants.*;
+import static org.openhab.core.thing.ThingStatus.OFFLINE;
+import static org.openhab.core.thing.ThingStatus.ONLINE;
+import static org.openhab.core.thing.ThingStatusDetail.*;
+
+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.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHACommunicationException;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAException;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAInvalidHostException;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHANotAuthorizedException;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHATimeoutException;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAUnauthorizedException;
+import org.openhab.binding.plugwiseha.internal.api.model.PlugwiseHAController;
+import org.openhab.binding.plugwiseha.internal.api.model.PlugwiseHAModel;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.GatewayInfo;
+import org.openhab.binding.plugwiseha.internal.config.PlugwiseHABridgeThingConfig;
+import org.openhab.binding.plugwiseha.internal.config.PlugwiseHAThingConfig;
+import org.openhab.binding.plugwiseha.internal.discovery.PlugwiseHADiscoveryService;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PlugwiseHABridgeHandler} class is responsible for handling
+ * commands and status updates for the Plugwise Home Automation bridge.
+ * Extends @{link BaseBridgeHandler}
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ * 
+ */
+
+@NonNullByDefault
+public class PlugwiseHABridgeHandler extends BaseBridgeHandler {
+
+    // Private Static error messages
+
+    private static final String STATUS_DESCRIPTION_COMMUNICATION_ERROR = "Error communicating with the Plugwise Home Automation controller";
+    private static final String STATUS_DESCRIPTION_TIMEOUT = "Communication timeout while communicating with the Plugwise Home Automation controller";
+    private static final String STATUS_DESCRIPTION_CONFIGURATION_ERROR = "Invalid or missing configuration";
+    private static final String STATUS_DESCRIPTION_INVALID_CREDENTIALS = "Invalid username and/or password - please double-check your configuration";
+    private static final String STATUS_DESCRIPTION_INVALID_HOSTNAME = "Invalid hostname - please double-check your configuration";
+
+    // Private member variables/constants
+    private @Nullable ScheduledFuture<?> refreshJob;
+    private @Nullable volatile PlugwiseHAController controller;
+
+    private final HttpClient httpClient;
+    private final Logger logger = LoggerFactory.getLogger(PlugwiseHABridgeHandler.class);
+
+    // Constructor
+
+    public PlugwiseHABridgeHandler(Bridge bridge, HttpClient httpClient) {
+        super(bridge);
+        this.httpClient = httpClient;
+    }
+
+    // Public methods
+
+    @Override
+    public void initialize() {
+        PlugwiseHABridgeThingConfig bridgeConfig = getConfigAs(PlugwiseHABridgeThingConfig.class);
+
+        if (this.checkConfig(bridgeConfig)) {
+            logger.debug("Initializing the Plugwise Home Automation bridge handler with config = {}", bridgeConfig);
+            try {
+                this.controller = new PlugwiseHAController(httpClient, bridgeConfig.getHost(), bridgeConfig.getPort(),
+                        bridgeConfig.getUsername(), bridgeConfig.getsmileId());
+                scheduleRefreshJob(bridgeConfig);
+            } catch (PlugwiseHAException e) {
+                updateStatus(OFFLINE, CONFIGURATION_ERROR, e.getMessage());
+            }
+        } else {
+            logger.warn("Invalid config for the Plugwise Home Automation bridge handler with config = {}",
+                    bridgeConfig);
+        }
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Collections.singleton(PlugwiseHADiscoveryService.class);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        this.logger.warn(
+                "Ignoring command = {} for channel = {} - this channel for the Plugwise Home Automation binding is read-only!",
+                command, channelUID);
+    }
+
+    @Override
+    public void dispose() {
+        cancelRefreshJob();
+        if (this.controller != null) {
+            this.controller = null;
+        }
+    }
+
+    public static boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_BRIDGE_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    // Getters & setters
+
+    public @Nullable PlugwiseHAController getController() {
+        return this.controller;
+    }
+
+    // Protected and private methods
+
+    /**
+     * Checks the configuration for validity, result is reflected in the status of
+     * the Thing
+     */
+    private boolean checkConfig(PlugwiseHABridgeThingConfig bridgeConfig) {
+        if (!bridgeConfig.isValid()) {
+            updateStatus(OFFLINE, CONFIGURATION_ERROR, STATUS_DESCRIPTION_CONFIGURATION_ERROR);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    private void scheduleRefreshJob(PlugwiseHABridgeThingConfig bridgeConfig) {
+        synchronized (this) {
+            if (this.refreshJob == null) {
+                logger.debug("Scheduling refresh job every {}s", bridgeConfig.getRefresh());
+                this.refreshJob = scheduler.scheduleWithFixedDelay(this::run, 0, bridgeConfig.getRefresh(),
+                        TimeUnit.SECONDS);
+            }
+        }
+    }
+
+    private void run() {
+        try {
+            logger.trace("Executing refresh job");
+            refresh();
+
+            if (super.thing.getStatus() == ThingStatus.INITIALIZING) {
+                setBridgeProperties();
+            }
+
+        } catch (PlugwiseHAInvalidHostException e) {
+            updateStatus(OFFLINE, CONFIGURATION_ERROR, STATUS_DESCRIPTION_INVALID_HOSTNAME);
+        } catch (PlugwiseHAUnauthorizedException | PlugwiseHANotAuthorizedException e) {
+            updateStatus(OFFLINE, CONFIGURATION_ERROR, STATUS_DESCRIPTION_INVALID_CREDENTIALS);
+        } catch (PlugwiseHACommunicationException e) {
+            updateStatus(OFFLINE, COMMUNICATION_ERROR, STATUS_DESCRIPTION_COMMUNICATION_ERROR);
+        } catch (PlugwiseHATimeoutException e) {
+            updateStatus(OFFLINE, COMMUNICATION_ERROR, STATUS_DESCRIPTION_TIMEOUT);
+        } catch (PlugwiseHAException e) {
+            updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getMessage());
+        } catch (RuntimeException e) {
+            updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getMessage());
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void refresh() throws PlugwiseHAException {
+        if (this.getController() != null) {
+            logger.debug("Refreshing the Plugwise Home Automation Controller {}", getThing().getUID());
+
+            PlugwiseHAController controller = this.getController();
+            if (controller != null) {
+                controller.refresh();
+                updateStatus(ONLINE);
+            }
+
+            getThing().getThings().forEach((thing) -> {
+                ThingHandler thingHandler = thing.getHandler();
+                if (thingHandler instanceof PlugwiseHABaseHandler) {
+                    ((PlugwiseHABaseHandler<PlugwiseHAModel, PlugwiseHAThingConfig>) thingHandler).refresh();
+                }
+            });
+        }
+    }
+
+    @SuppressWarnings("null")
+    private void cancelRefreshJob() {
+        synchronized (this) {
+            if (this.refreshJob != null) {
+                logger.debug("Cancelling refresh job");
+                this.refreshJob.cancel(true);
+                this.refreshJob = null;
+            }
+        }
+    }
+
+    protected void setBridgeProperties() {
+        logger.debug("Setting bridge properties");
+        try {
+            PlugwiseHAController controller = this.getController();
+            GatewayInfo localGatewayInfo = null;
+            if (controller != null) {
+                localGatewayInfo = controller.getGatewayInfo();
+            }
+
+            if (localGatewayInfo != null) {
+                Map<String, String> properties = editProperties();
+                if (localGatewayInfo.getFirmwareVersion() != null) {
+                    properties.put(Thing.PROPERTY_FIRMWARE_VERSION, localGatewayInfo.getFirmwareVersion());
+                }
+                if (localGatewayInfo.getHardwareVersion() != null) {
+                    properties.put(Thing.PROPERTY_HARDWARE_VERSION, localGatewayInfo.getHardwareVersion());
+                }
+                if (localGatewayInfo.getMacAddress() != null) {
+                    properties.put(Thing.PROPERTY_MAC_ADDRESS, localGatewayInfo.getMacAddress());
+                }
+                if (localGatewayInfo.getVendorName() != null) {
+                    properties.put(Thing.PROPERTY_VENDOR, localGatewayInfo.getVendorName());
+                }
+                if (localGatewayInfo.getVendorModel() != null) {
+                    properties.put(Thing.PROPERTY_MODEL_ID, localGatewayInfo.getVendorModel());
+                }
+
+                updateProperties(properties);
+            }
+        } catch (PlugwiseHAException e) {
+            updateStatus(OFFLINE, COMMUNICATION_ERROR, STATUS_DESCRIPTION_COMMUNICATION_ERROR);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/handler/PlugwiseHAZoneHandler.java b/bundles/org.openhab.binding.plugwiseha/src/main/java/org/openhab/binding/plugwiseha/internal/handler/PlugwiseHAZoneHandler.java
new file mode 100644 (file)
index 0000000..cfc0a2c
--- /dev/null
@@ -0,0 +1,222 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.plugwiseha.internal.handler;
+
+import static org.openhab.binding.plugwiseha.internal.PlugwiseHABindingConstants.*;
+import static org.openhab.core.thing.ThingStatus.*;
+import static org.openhab.core.thing.ThingStatusDetail.BRIDGE_OFFLINE;
+import static org.openhab.core.thing.ThingStatusDetail.COMMUNICATION_ERROR;
+import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR;
+
+import java.util.Map;
+import java.util.Optional;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Temperature;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.plugwiseha.internal.PlugwiseHABindingConstants;
+import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAException;
+import org.openhab.binding.plugwiseha.internal.api.model.PlugwiseHAController;
+import org.openhab.binding.plugwiseha.internal.api.model.dto.Location;
+import org.openhab.binding.plugwiseha.internal.config.PlugwiseHAThingConfig;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PlugwiseHAZoneHandler} class is responsible for handling commands
+ * and status updates for the Plugwise Home Automation zones/locations.
+ * Extends @{link PlugwiseHABaseHandler}
+ *
+ * @author Bas van Wetten - Initial contribution
+ * @author Leo Siepel - finish initial contribution
+ *
+ */
+
+@NonNullByDefault
+public class PlugwiseHAZoneHandler extends PlugwiseHABaseHandler<Location, PlugwiseHAThingConfig> {
+
+    private @Nullable Location location;
+    private final Logger logger = LoggerFactory.getLogger(PlugwiseHAZoneHandler.class);
+
+    // Constructor
+
+    public PlugwiseHAZoneHandler(Thing thing) {
+        super(thing);
+    }
+
+    public static boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return PlugwiseHABindingConstants.THING_TYPE_ZONE.equals(thingTypeUID);
+    }
+
+    // Overrides
+
+    @Override
+    protected synchronized void initialize(PlugwiseHAThingConfig config, PlugwiseHABridgeHandler bridgeHandler) {
+        if (thing.getStatus() == INITIALIZING) {
+            logger.debug("Initializing Plugwise Home Automation zone handler with config = {}", config);
+            if (!config.isValid()) {
+                updateStatus(OFFLINE, CONFIGURATION_ERROR,
+                        "Invalid configuration for Plugwise Home Automation zone handler.");
+                return;
+            }
+
+            try {
+                PlugwiseHAController controller = bridgeHandler.getController();
+                if (controller != null) {
+                    this.location = getEntity(controller, true);
+                    if (this.location != null) {
+                        setLocationProperties();
+                        updateStatus(ONLINE);
+                    } else {
+                        updateStatus(OFFLINE);
+                    }
+                } else {
+                    updateStatus(OFFLINE, BRIDGE_OFFLINE);
+                }
+            } catch (PlugwiseHAException e) {
+                updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getMessage());
+            }
+        }
+    }
+
+    @Override
+    protected @Nullable Location getEntity(PlugwiseHAController controller, Boolean forceRefresh)
+            throws PlugwiseHAException {
+        PlugwiseHAThingConfig config = getPlugwiseThingConfig();
+        Location location = controller.getLocation(config.getId(), forceRefresh);
+
+        return location;
+    }
+
+    @Override
+    protected void handleCommand(Location entity, ChannelUID channelUID, Command command) throws PlugwiseHAException {
+        String channelID = channelUID.getIdWithoutGroup();
+        PlugwiseHABridgeHandler bridge = this.getPlugwiseHABridge();
+        if (bridge != null) {
+            PlugwiseHAController controller = bridge.getController();
+            if (controller != null) {
+                switch (channelID) {
+                    case ZONE_SETPOINT_CHANNEL:
+                        if (command instanceof QuantityType) {
+                            Unit<Temperature> unit = entity.getSetpointTemperatureUnit().orElse(UNIT_CELSIUS)
+                                    .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
+                            QuantityType<?> state = ((QuantityType<?>) command).toUnit(unit);
+                            if (state != null) {
+                                try {
+                                    controller.setLocationThermostat(entity, state.doubleValue());
+                                } catch (PlugwiseHAException e) {
+                                    logger.warn("Unable to update setpoint for zone '{}': {} -> {}", entity.getName(),
+                                            entity.getSetpointTemperature().orElse(null), state.doubleValue());
+                                }
+                            }
+                        }
+                        break;
+                    case ZONE_PREHEAT_CHANNEL:
+                        if (command instanceof OnOffType) {
+                            try {
+                                controller.setPreHeating(entity, command == OnOffType.ON);
+                            } catch (PlugwiseHAException e) {
+                                logger.warn("Unable to switch zone pre heating {} for zone '{}'", (State) command,
+                                        entity.getName());
+                            }
+                        }
+                        break;
+                    default:
+                        logger.warn("Ignoring unsupported command = {} for channel = {}", command, channelUID);
+                }
+            }
+        }
+    }
+
+    private State getDefaultState(String channelID) {
+        State state = UnDefType.NULL;
+        switch (channelID) {
+            case ZONE_PREHEAT_CHANNEL:
+            case ZONE_PRESETSCENE_CHANNEL:
+            case ZONE_SETPOINT_CHANNEL:
+            case ZONE_TEMPERATURE_CHANNEL:
+                state = UnDefType.NULL;
+                break;
+        }
+        return state;
+    }
+
+    @Override
+    protected void refreshChannel(Location entity, ChannelUID channelUID) {
+        String channelID = channelUID.getIdWithoutGroup();
+        State state = getDefaultState(channelID);
+
+        switch (channelID) {
+            case ZONE_PREHEAT_CHANNEL:
+                Optional<Boolean> preHeatState = entity.getPreHeatState();
+                if (preHeatState.isPresent()) {
+                    state = OnOffType.from(preHeatState.get());
+                }
+                break;
+            case ZONE_PRESETSCENE_CHANNEL:
+                state = new StringType(entity.getPreset());
+                break;
+            case ZONE_SETPOINT_CHANNEL:
+                if (entity.getSetpointTemperature().isPresent()) {
+                    Unit<Temperature> unit = entity.getSetpointTemperatureUnit().orElse(UNIT_CELSIUS)
+                            .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
+                    state = new QuantityType<Temperature>(entity.getSetpointTemperature().get(), unit);
+                }
+                break;
+            case ZONE_TEMPERATURE_CHANNEL:
+                if (entity.getTemperature().isPresent()) {
+                    Unit<Temperature> unit = entity.getTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
+                            ? SIUnits.CELSIUS
+                            : ImperialUnits.FAHRENHEIT;
+                    state = new QuantityType<Temperature>(entity.getTemperature().get(), unit);
+                }
+                break;
+            default:
+                break;
+        }
+
+        if (state != UnDefType.NULL) {
+            updateState(channelID, state);
+        }
+    }
+
+    protected void setLocationProperties() {
+        if (this.location != null) {
+            Map<String, String> properties = editProperties();
+
+            Location localLocation = this.location;
+            if (localLocation != null) {
+                properties.put(PlugwiseHABindingConstants.LOCATION_PROPERTY_DESCRIPTION,
+                        localLocation.getDescription());
+                properties.put(PlugwiseHABindingConstants.LOCATION_PROPERTY_TYPE, localLocation.getType());
+                properties.put(PlugwiseHABindingConstants.LOCATION_PROPERTY_FUNCTIONALITIES,
+                        String.join(", ", localLocation.getActuatorFunctionalities().keySet()));
+            }
+
+            updateProperties(properties);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.plugwiseha/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..ffb50a3
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="plugwiseha" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>Plugwise Home Automation Binding</name>
+       <description>This binding supports the Plugwise Home Automation 'Adam' gateway. It allows users to access temperature
+               controls of zones defined on the gateway</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.plugwiseha/src/main/resources/OH-INF/config/config.xml
new file mode 100644 (file)
index 0000000..712cbce
--- /dev/null
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <!-- Bridge -->
+       <config-description uri="bridge-type:plugwiseha:gateway">
+               <parameter name="host" type="text" required="true">
+                       <context>network-address</context>
+                       <label>Host</label>
+                       <description>Hostname or IP address of the boiler gateway</description>
+                       <default>adam</default>
+               </parameter>
+               <parameter name="username" type="text" required="true">
+                       <label>Username</label>
+                       <description>Adam HA gateway username (default: smile)</description>
+                       <default>smile</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="smileId" type="text" pattern="[a-zA-Z0-9]{8}" required="true">
+                       <context>password</context>
+                       <label>Smile ID</label>
+                       <description>The Smile ID is the 8 letter code on the sticker on the back of the Adam boiler gateway</description>
+               </parameter>
+               <parameter name="refresh" type="integer" min="1" max="120" required="true" unit="s">
+                       <label>Refresh Interval</label>
+                       <unitLabel>seconds</unitLabel>
+                       <description>Refresh interval in seconds</description>
+                       <default>5</default>
+                       <advanced>true</advanced>
+               </parameter>
+       </config-description>
+
+       <!-- Zone thing -->
+       <config-description uri="thing-type:plugwiseha:zone">
+               <parameter name="id" type="text" required="true" readOnly="false">
+                       <label>ID</label>
+                       <description>Location ID for the zone</description>
+               </parameter>
+       </config-description>
+
+       <config-description uri="thing-type:plugwiseha:appliance_boiler">
+               <parameter name="id" type="text" required="true" readOnly="false">
+                       <label>ID</label>
+                       <description>Appliance ID</description>
+               </parameter>
+       </config-description>
+
+       <!-- Appliance: Radiator valve -->
+       <config-description uri="thing-type:plugwiseha:appliance_valve">
+               <parameter name="id" type="text" required="true" readOnly="false">
+                       <label>ID</label>
+                       <description>Appliance ID</description>
+               </parameter>
+               <parameter name="lowBatteryPercentage" type="integer" min="1" max="50" required="true">
+                       <label>Low Battery Threshold</label>
+                       <unitLabel>%</unitLabel>
+                       <description>Battery charge remaining at which to trigger battery low warning</description>
+                       <default>15</default>
+                       <advanced>true</advanced>
+               </parameter>
+       </config-description>
+
+       <!-- Appliance: Pump switch -->
+       <config-description uri="thing-type:plugwiseha:appliance_pump">
+               <parameter name="id" type="text" required="true" readOnly="false">
+                       <label>ID</label>
+                       <description>Appliance ID</description>
+               </parameter>
+       </config-description>
+
+       <!-- Appliance: Radiator valve -->
+       <config-description uri="thing-type:plugwiseha:appliance_thermostat">
+               <parameter name="id" type="text" required="true" readOnly="false">
+                       <label>ID</label>
+                       <description>Appliance ID</description>
+               </parameter>
+               <parameter name="lowBatteryPercentage" type="integer" min="1" max="50" required="true">
+                       <label>Low Battery Threshold</label>
+                       <unitLabel>%</unitLabel>
+                       <description>Battery charge remaining at which to trigger battery low warning</description>
+                       <default>15</default>
+                       <advanced>true</advanced>
+               </parameter>
+       </config-description>
+
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.plugwiseha/src/main/resources/OH-INF/thing/channels.xml
new file mode 100644 (file)
index 0000000..7f57acc
--- /dev/null
@@ -0,0 +1,194 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="plugwiseha"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <channel-type id="setpointTemperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Setpoint Temperature</label>
+               <description>Gets or sets the set point of this zone</description>
+               <category>heating</category>
+               <state min="0.0" max="35.0" step="0.5" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="temperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Zone Temperature</label>
+               <description>Gets the temperature of this zone</description>
+               <category>heating</category>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="offsetTemperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Thermostat Temperature Offset</label>
+               <description>Gets or sets the temperature offset for this thermostat</description>
+               <category>heating</category>
+               <state pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="preHeat">
+               <item-type>Switch</item-type>
+               <label>Preheat</label>
+               <description>Switch the preheating of a zone ON or OFF</description>
+               <category>switch</category>
+       </channel-type>
+
+       <channel-type id="power">
+               <item-type>Switch</item-type>
+               <label>Power</label>
+               <description>Switch the Plugwise Smart plug ON or OFF</description>
+               <category>switch</category>
+       </channel-type>
+
+       <channel-type id="lock">
+               <item-type>Switch</item-type>
+               <label>Lock</label>
+               <description>Locks the switch state of the Plugwise Smart plug</description>
+               <category>switch</category>
+       </channel-type>
+
+       <channel-type id="powerUsage">
+               <item-type>Number:Power</item-type>
+               <label>Power Usage</label>
+               <state pattern="%.2f %unit%" readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="chState">
+               <item-type>Switch</item-type>
+               <label>Central Heating Active</label>
+               <description>Is the boiler active for central heating, On or OFF</description>
+               <category>switch</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="dhwState">
+               <item-type>Switch</item-type>
+               <label>Domestic Hot Water Active</label>
+               <description>Is the boiler active for domestic hot water, On or OFF</description>
+               <category>switch</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="coolingState">
+               <item-type>Switch</item-type>
+               <label>Cooling State</label>
+               <description>Is the boiler active for cooling, On or OFF</description>
+               <category>switch</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="flameState">
+               <item-type>Switch</item-type>
+               <label>Flame State</label>
+               <description>Is the boiler's flame active, On or OFF</description>
+               <category>switch</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="intendedHeatingState">
+               <item-type>Switch</item-type>
+               <label>Intended Heating State</label>
+               <description>Should the boiler be active for central heating, On or OFF</description>
+               <category>switch</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="dhwComfortMode">
+               <item-type>Switch</item-type>
+               <label>Domestic Hot Water Comfort Mode</label>
+               <description>Is the boiler's domestic hot water mode set to comfort, On or OFF</description>
+               <category>switch</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="intendedBoilerTemp">
+               <item-type>Number:Temperature</item-type>
+               <label>Intended Boiler Temperature</label>
+               <description>Gets the intended temperature of this boiler</description>
+               <category>heating</category>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="modulationLevel">
+               <item-type>Number</item-type>
+               <label>Modulelation Level</label>
+               <description>Gets the modulation level of this boiler</description>
+               <category>heating</category>
+               <state readOnly="true" pattern="%.0f"/>
+       </channel-type>
+
+       <channel-type id="otAppFaultCode">
+               <item-type>Number</item-type>
+               <label>Opentherm Application Faultcode</label>
+               <description>Gets the Opentherm application fault code of this boiler</description>
+               <category>heating</category>
+               <state readOnly="true" pattern="%.0f"/>
+       </channel-type>
+
+       <channel-type id="dhwTemperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Domestic Hot Water Temperature</label>
+               <description>Gets the temperature of the domestic hot water</description>
+               <category>heating</category>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="otOEMFaultCode">
+               <item-type>Number</item-type>
+               <label>OEM Fault Code</label>
+               <description>Gets the OEM fault code of this boiler</description>
+               <category>heating</category>
+               <state readOnly="true" pattern="%.0f"/>
+       </channel-type>
+
+       <channel-type id="boilerTemperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Boiler Temperature</label>
+               <description>Gets the temperature of this boiler</description>
+               <category>heating</category>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="dhwSetpoint">
+               <item-type>Number:Temperature</item-type>
+               <label>Domestic Hot Water Setpoint Temperature</label>
+               <description>Gets the temperature of the domestic hot water setpoint</description>
+               <category>heating</category>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="maxBoilerTemperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Max Boiler Temperature</label>
+               <description>Gets the maximum temperature ofthis boiler</description>
+               <category>heating</category>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="waterPressure">
+               <item-type>Number:Pressure</item-type>
+               <label>Water Pressure</label>
+               <description>Gets the water pressure of the boiler</description>
+               <category>heating</category>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="presetScene">
+               <item-type>String</item-type>
+               <label>Preset Scene</label>
+               <description>Gets the preset scene of the zone</description>
+               <category>heating</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="valvePosition">
+               <item-type>Number</item-type>
+               <label>Valve Position</label>
+               <description>Gets the position of the valve (0% closed, 100% open)</description>
+               <category>heating</category>
+               <state readOnly="true" pattern="%.0f"/>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.plugwiseha/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..11d3b11
--- /dev/null
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="plugwiseha"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- Bridge -->
+       <bridge-type id="gateway">
+               <label>Plugwise Home Automation Bridge</label>
+               <description>The Plugwise Home Automation Bridge is needed to connect to the Adam boiler gateway</description>
+
+               <config-description-ref uri="bridge-type:plugwiseha:gateway"/>
+       </bridge-type>
+
+       <!-- Zone thing -->
+       <thing-type id="appliance_boiler" listed="true">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="gateway"/>
+               </supported-bridge-type-refs>
+
+               <label>Boiler</label>
+               <description>A Plugwise Home Automation controlled boiler</description>
+
+               <channels>
+                       <channel id="chState" typeId="chState"/>
+                       <channel id="dhwState" typeId="dhwState"/>
+                       <channel id="waterPressure" typeId="waterPressure"/>
+                       <channel id="coolingState" typeId="coolingState"/>
+                       <channel id="flameState" typeId="flameState"/>
+                       <channel id="intendedHeatingState" typeId="intendedHeatingState"/>
+                       <channel id="dhwComfortMode" typeId="dhwComfortMode"/>
+                       <channel id="intendedBoilerTemp" typeId="intendedBoilerTemp"/>
+                       <channel id="modulationLevel" typeId="modulationLevel"/>
+                       <channel id="otAppFaultCode" typeId="otAppFaultCode"/>
+                       <channel id="dhwTemperature" typeId="dhwTemperature"/>
+                       <channel id="otOEMFaultCode" typeId="otOEMFaultCode"/>
+                       <channel id="boilerTemperature" typeId="boilerTemperature"/>
+                       <channel id="dhwSetpoint" typeId="dhwSetpoint"/>
+                       <channel id="maxBoilerTemperature" typeId="maxBoilerTemperature"/>
+               </channels>
+
+               <representation-property>id</representation-property>
+
+               <config-description-ref uri="thing-type:plugwiseha:appliance_boiler"/>
+       </thing-type>
+
+       <!-- Zone thing -->
+       <thing-type id="zone" listed="true">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="gateway"/>
+               </supported-bridge-type-refs>
+
+               <label>Plugwise Zone</label>
+               <description>A Plugwise Home Automation heating zone</description>
+
+               <channels>
+                       <channel id="setpointTemperature" typeId="setpointTemperature"/>
+                       <channel id="temperature" typeId="temperature"/>
+                       <channel id="presetScene" typeId="presetScene"/>
+                       <channel id="preHeat" typeId="preHeat"/>
+               </channels>
+
+               <representation-property>id</representation-property>
+
+               <config-description-ref uri="thing-type:plugwiseha:zone"/>
+       </thing-type>
+
+       <!-- Appliance: Radiator valve (Tom) -->
+       <thing-type id="appliance_valve" listed="true">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="gateway"/>
+               </supported-bridge-type-refs>
+
+               <label>Plugwise Radiator Valve</label>
+               <description>A Plugwise Home Automation radiator valve</description>
+
+               <channels>
+                       <channel id="setpointTemperature" typeId="setpointTemperature"/>
+                       <channel id="temperature" typeId="temperature"/>
+                       <channel id="valvePosition" typeId="valvePosition"/>
+               </channels>
+
+               <representation-property>id</representation-property>
+
+               <config-description-ref uri="thing-type:plugwiseha:appliance_valve"/>
+       </thing-type>
+
+       <!-- Appliance: Pump switch (Circle) -->
+       <thing-type id="appliance_pump" listed="true">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="gateway"/>
+               </supported-bridge-type-refs>
+
+               <label>Central Heating Pump</label>
+               <description>A Plugwise Home Automation smart plug switch connected to a central heating pump</description>
+
+               <channels>
+                       <channel id="power" typeId="power"/>
+                       <channel id="lock" typeId="lock"/>
+                       <channel id="powerUsage" typeId="powerUsage"/>
+               </channels>
+
+               <representation-property>id</representation-property>
+
+               <config-description-ref uri="thing-type:plugwiseha:appliance_pump"/>
+       </thing-type>
+
+       <!-- Appliance: Zone thermostat (Lisa) -->
+       <thing-type id="appliance_thermostat" listed="true">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="gateway"/>
+               </supported-bridge-type-refs>
+
+               <label>Plugwise Room Thermostat</label>
+               <description>A Plugwise Home Automation room thermostat</description>
+
+               <channels>
+                       <channel id="setpointTemperature" typeId="setpointTemperature"/>
+                       <channel id="temperature" typeId="temperature"/>
+                       <channel id="offsetTemperature" typeId="offsetTemperature"/>
+               </channels>
+
+               <representation-property>id</representation-property>
+
+               <config-description-ref uri="thing-type:plugwiseha:appliance_thermostat"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.plugwiseha/src/main/resources/domain_objects.xslt b/bundles/org.openhab.binding.plugwiseha/src/main/resources/domain_objects.xslt
new file mode 100644 (file)
index 0000000..8b1be13
--- /dev/null
@@ -0,0 +1,127 @@
+    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
+        <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
+        <xsl:strip-space elements="*"/>
+
+        <!-- modified identity transform -->
+        <xsl:template match="/domain_objects">
+            <xsl:element name="{local-name()}">        
+                <xsl:apply-templates select="gateway" />
+                <xsl:apply-templates select="appliance" />
+                <xsl:apply-templates select="location" />
+                <xsl:apply-templates select="module" />
+            </xsl:element>
+        </xsl:template>
+
+        <xsl:template match="node()">
+            <!-- prevent duplicate siblings -->
+            <xsl:if test="count(preceding-sibling::node()[name()=name(current())])=0">
+                <!-- copy element -->
+                <xsl:copy>
+                    <xsl:apply-templates select="@*|node()"/>
+                </xsl:copy>
+            </xsl:if>        
+        </xsl:template>
+
+        <xsl:template match="appliance">
+            <!-- copy element -->
+            <xsl:copy>
+                <xsl:apply-templates select="@*|node()"/>
+            </xsl:copy>     
+        </xsl:template>
+
+        <xsl:template match="location">
+            <!-- copy element -->
+            <xsl:copy>
+                <xsl:apply-templates select="@*|node()"/>
+            </xsl:copy>     
+        </xsl:template>
+
+        <xsl:template match="module">            
+            <!-- copy element -->
+            <xsl:copy>
+                <xsl:apply-templates select="protocols/node()[name()='zig_bee_node']"/>
+                <xsl:apply-templates select="@*|node()[name()!='protocols']"/>
+            </xsl:copy>     
+        </xsl:template>
+
+        <xsl:template match="location/appliances">
+            <!-- Apply identity transform on child elements of appliances -->
+            <xsl:for-each select="appliance">
+                <xsl:copy>
+                    <xsl:value-of select="@id"/>
+                </xsl:copy>
+            </xsl:for-each>
+        </xsl:template>
+
+        <xsl:template match="module/services">
+            <xsl:for-each select="./node()">
+                <xsl:element name="service">
+                    <xsl:element name="point_log">
+                        <xsl:value-of select="functionalities/point_log/@id"/>
+                    </xsl:element>                    
+                    <xsl:apply-templates select="@*|node()[name()!='functionalities']"/>
+                </xsl:element>
+            </xsl:for-each>
+        </xsl:template>
+
+        <!-- This matches 'appliance/logs' or 'location/logs' -->
+        <xsl:template match="*[name() = 'location' or name()='appliance']/logs">        
+            <!-- Apply identity transform on child elements of logs -->
+            <xsl:variable name="meter_id" select="point_log/*[substring(local-name(), string-length(local-name()) - string-length('_meter')+1) = '_meter']/@id"/>            
+            <xsl:apply-templates select="/domain_objects/module/services/*[@id=$meter_id]/../../protocols/zig_bee_node"/>
+            
+            <xsl:for-each select="point_log">            
+                <xsl:copy>                
+                    <xsl:apply-templates select="@*|node()"/>
+                </xsl:copy>
+            </xsl:for-each>
+        </xsl:template>
+
+        <xsl:template match="appliance/location">
+            <!-- Apply identity transform on child elements of location -->        
+            <xsl:copy>
+                <xsl:value-of select="@id"/>
+            </xsl:copy>        
+        </xsl:template>
+
+        <xsl:template match="logs/point_log/period">    
+            <xsl:element name="measurement_date">
+                <xsl:value-of select="measurement/@log_date"/>
+            </xsl:element>
+            <xsl:element name="measurement">
+                <xsl:value-of select="measurement/text()"/>
+            </xsl:element>
+        </xsl:template>
+
+        <xsl:template match="*[name() = 'location' or name()='appliance']/actuator_functionalities">
+            <xsl:for-each select="./*">
+                <xsl:element name="actuator_functionality">
+                    <xsl:if test="not(type)">
+                    <xsl:choose>
+                            <xsl:when test="local-name()='relay_functionality'">
+                                <xsl:element name="type">
+                                    <xsl:text>relay</xsl:text>
+                                </xsl:element>
+                            </xsl:when>
+                            <xsl:otherwise>
+                                <xsl:element name="type">
+                                    <xsl:value-of select="local-name()"/>
+                                </xsl:element>
+                            </xsl:otherwise>
+                        </xsl:choose>                    
+                    </xsl:if>
+                    <xsl:for-each select=".">
+                        <xsl:apply-templates select="@*|node()"/>
+                    </xsl:for-each>
+                </xsl:element>            
+            </xsl:for-each>
+        </xsl:template>
+
+        <!-- attributes to elements -->
+        <xsl:template match="@*">
+            <xsl:element name="{name()}">
+                <xsl:value-of select="."/>
+            </xsl:element>
+        </xsl:template>
+
+    </xsl:stylesheet>
\ No newline at end of file
index dbc247649ef8f3e17ca670babece5a3fdb13f49e..55732ad1a6f3eb977ea9bd39ec4f3f9fa1148e76 100644 (file)
     <module>org.openhab.binding.playstation</module>
     <module>org.openhab.binding.plclogo</module>
     <module>org.openhab.binding.plugwise</module>
+    <module>org.openhab.binding.plugwiseha</module>
     <module>org.openhab.binding.powermax</module>
     <module>org.openhab.binding.pulseaudio</module>
     <module>org.openhab.binding.pushbullet</module>