/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
<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>
--- /dev/null
+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
--- /dev/null
+# 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
+ }
+}
+```
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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();
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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() {
+ }
+}
--- /dev/null
+/**
+ * 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() {
+ }
+}
--- /dev/null
+/**
+ * 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() {
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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) {
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+}
--- /dev/null
+/**
+ * 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);
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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) {
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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());
+ }
+}
--- /dev/null
+/**
+ * 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 + "}";
+ }
+}
--- /dev/null
+/**
+ * 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 + "}";
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+ <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
<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>