From 85dedbde58ce6fc86f2caedeed665dcd9a624af0 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 24 Jun 2020 08:26:00 -0700 Subject: [PATCH 01/85] [max] increase delay (#7987) This prevents excessive updates for temperature changes and restores the old behaviour Fixes issue in: https://community.openhab.org/t/basic-ui-setpoint-issue/101167 Signed-off-by: Marcel Verpaalen --- .../binding/max/internal/handler/MaxCubeBridgeHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/handler/MaxCubeBridgeHandler.java b/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/handler/MaxCubeBridgeHandler.java index 59dbe809f1606..4f5e1a7cc43f7 100644 --- a/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/handler/MaxCubeBridgeHandler.java +++ b/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/handler/MaxCubeBridgeHandler.java @@ -381,7 +381,7 @@ public void run() { logger.debug("Error sending command {} to MAX! Cube at IP: {}", sendCommand, ipAddress); } } - Thread.sleep(50); + Thread.sleep(5000); } } catch (InterruptedException e) { logger.debug("Stopping queueConsumer"); From e9079b075d85c48b2e4167c03d2ecaddb773583f Mon Sep 17 00:00:00 2001 From: J-N-K Date: Wed, 24 Jun 2020 21:41:39 +0200 Subject: [PATCH 02/85] [deconz] Re-add battery channel (#8005) Fixes #7920 Signed-off-by: Jan N. Klug --- .../binding/deconz/internal/handler/SensorThingHandler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java index bf3e1d00faafa..9d89726b6b828 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java @@ -102,6 +102,9 @@ protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { protected void valueUpdated(String channelID, SensorState newState, boolean initializing) { super.valueUpdated(channelID, newState, initializing); switch (channelID) { + case CHANNEL_BATTERY_LEVEL: + updateDecimalTypeChannel(channelID, newState.battery); + break; case CHANNEL_LIGHT: Boolean dark = newState.dark; if (dark != null) { From ef0d11cf186bd7c3fa8044e8dd721650cc29d36e Mon Sep 17 00:00:00 2001 From: mlobstein Date: Wed, 24 Jun 2020 15:02:10 -0500 Subject: [PATCH 03/85] [radiothermostat] RadioThermostat Binding - initial contribution (#7266) Signed-off-by: Michael Lobstein --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../.classpath | 32 ++ .../.project | 23 + .../NOTICE | 13 + .../README.md | 202 ++++++++ .../pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../RadioThermostatBindingConstants.java | 84 ++++ .../RadioThermostatConfiguration.java | 31 ++ .../RadioThermostatHandlerFactory.java | 71 +++ .../RadioThermostatHttpException.java | 29 ++ ...dioThermostatStateDescriptionProvider.java | 64 +++ .../internal/RadioThermostatThingActions.java | 68 +++ .../RadioThermostatConnector.java | 173 +++++++ .../communication/RadioThermostatEvent.java | 44 ++ .../RadioThermostatEventListener.java | 33 ++ .../RadioThermostatDiscoveryService.java | 284 +++++++++++ .../internal/dto/RadioThermostatDTO.java | 52 ++ .../dto/RadioThermostatHumidityDTO.java | 33 ++ .../dto/RadioThermostatRuntimeDTO.java | 51 ++ .../RadioThermostatRuntimeHeatCoolDTO.java | 51 ++ .../internal/dto/RadioThermostatTimeDTO.java | 92 ++++ .../internal/dto/RadioThermostatTstatDTO.java | 148 ++++++ .../handler/RadioThermostatHandler.java | 443 ++++++++++++++++++ .../resources/ESH-INF/binding/binding.xml | 10 + .../resources/ESH-INF/thing/thing-types.xml | 207 ++++++++ bundles/pom.xml | 1 + 28 files changed, 2271 insertions(+) create mode 100644 bundles/org.openhab.binding.radiothermostat/.classpath create mode 100644 bundles/org.openhab.binding.radiothermostat/.project create mode 100644 bundles/org.openhab.binding.radiothermostat/NOTICE create mode 100644 bundles/org.openhab.binding.radiothermostat/README.md create mode 100644 bundles/org.openhab.binding.radiothermostat/pom.xml create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatBindingConstants.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatConfiguration.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatHandlerFactory.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatHttpException.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatStateDescriptionProvider.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatThingActions.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatConnector.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatEvent.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatEventListener.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/discovery/RadioThermostatDiscoveryService.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatDTO.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatHumidityDTO.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatRuntimeDTO.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatRuntimeHeatCoolDTO.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatTimeDTO.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatTstatDTO.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/handler/RadioThermostatHandler.java create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/resources/ESH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.radiothermostat/src/main/resources/ESH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index 13b48f0bd423b..9e56bb4d5b467 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -164,6 +164,7 @@ /bundles/org.openhab.binding.powermax/ @lolodomo /bundles/org.openhab.binding.pulseaudio/ @peuter /bundles/org.openhab.binding.pushbullet/ @hakan42 +/bundles/org.openhab.binding.radiothermostat/ @mlobstein /bundles/org.openhab.binding.regoheatpump/ @crnjan /bundles/org.openhab.binding.rfxcom/ @martinvw @paulianttila /bundles/org.openhab.binding.rme/ @kgoderis diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index e8eec5fe346bf..263cd1c063409 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -809,6 +809,11 @@ org.openhab.binding.pushbullet ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.radiothermostat + ${project.version} + org.openhab.addons.bundles org.openhab.binding.regoheatpump diff --git a/bundles/org.openhab.binding.radiothermostat/.classpath b/bundles/org.openhab.binding.radiothermostat/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.binding.radiothermostat/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.radiothermostat/.project b/bundles/org.openhab.binding.radiothermostat/.project new file mode 100644 index 0000000000000..6efb0cb2923e5 --- /dev/null +++ b/bundles/org.openhab.binding.radiothermostat/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.radiothermostat + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.radiothermostat/NOTICE b/bundles/org.openhab.binding.radiothermostat/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.radiothermostat/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.radiothermostat/README.md b/bundles/org.openhab.binding.radiothermostat/README.md new file mode 100644 index 0000000000000..ab307b4cd4904 --- /dev/null +++ b/bundles/org.openhab.binding.radiothermostat/README.md @@ -0,0 +1,202 @@ +# RadioThermostat Binding + +This binding connects RadioThermostat/3M Filtrete models CT30, CT50/3M50, CT80, etc. with built-in Wi-Fi module to openHAB. + +The binding retrieves and periodically updates all basic system information from the thermostat. +The main thermostat functions such as thermostat mode, fan mode, temperature set point and hold mode can be controlled. +System run-time information and humidity readings are polled less frequently and can be disabled completely if not desired. +Humidity information is available only when using a CT80 thermostat and I have noticed that the humidity reported is very inaccurate. + +The main caveat for using this binding is to keep in mind that the web server in the thermostat is very slow. +Do not over load it with excessive amounts of simultaneous commands. +When changing the thermostat mode, the current temperature set point is cleared and a refresh of the thermostat data is done to get the new mode's set point. +Since retrieving the thermostat's data is the slowest operation, it will take several seconds after changing the mode before the new set point is displayed. +The 'Program Mode' command is untested and according to the published API is only available on a CT80 Rev B. + +## Supported Things + +There is exactly one supported thing type, which represents the thermostat. +It has the `rtherm` id. +Multiple Things can be added if more than one thermostat is to be controlled. + +## Discovery + +Auto-discovery is supported if the thermostat can be located on the local network using SSDP. +Otherwise the thing must be manually added. + +## Binding Configuration + +The binding has no configuration options, all configuration is done at Thing level. + +## Thing Configuration + +The thing has a few configuration parameters: + +| Parameter | Description | +|-----------------|-----------------------------------------------------------------------------------------------------------| +| hostName | The host name or IP address of the thermostat. Mandatory. | +| refresh | Overrides the refresh interval of the thermostat data. Optional, the default is 2 minutes. | +| logRefresh | Overrides the refresh interval of the run-time logs & humidity data. Optional, the default is 10 minutes. | +| isCT80 | Flag to enable additional features only available on the CT80 thermostat. Optional, the default is false. | +| disableLogs | Disable retrieval of run-time logs from the thermostat. Optional, the default is false. | + +## Channels + +The thermostat information that is retrieved is available as these channels: + +| Channel ID | Item Type | Description | +|------------------------|----------------------|---------------------------------------------------------------------------| +| temperature | Number:Temperature | The current temperature reading of the thermostat | +| humidity | Number:Dimensionless | The current humidity reading of the thermostat (CT80 only) | +| mode | Number | The current operating mode of the HVAC system | +| fan_mode | Number | The current operating mode of the fan | +| program_mode | Number | The program schedule that the thermostat is running (CT80 Rev B only) | +| set_point | Number:Temperature | The current temperature set point of the thermostat | +| status | Number | Indicates the current running status of the HVAC system | +| fan_status | Number | Indicates the current fan status of the HVAC system | +| override | Number | Indicates if the normal program set-point has been manually overridden | +| hold | Switch | Indicates if the current set point temperature is to be held indefinitely | +| day | Number | The current day of the week reported by the thermostat (0 = Monday) | +| hour | Number | The current hour of the day reported by the thermostat (24 hr) | +| minute | Number | The current minute past the hour reported by the thermostat | +| dt_stamp | String | The current day of the week and time reported by the thermostat (E HH:mm) | +| today_heat_runtime | Number:Time | The total number of minutes of heating run-time today | +| today_cool_runtime | Number:Time | The total number of minutes of cooling run-time today | +| yesterday_heat_runtime | Number:Time | The total number of minutes of heating run-time yesterday | +| yesterday_cool_runtime | Number:Time | The total number of minutes of cooling run-time yesterday | + +## Full Example + +radiotherm.map: + +```text +UNDEF_stus=- +NULL_stus=- +-_stus=- +0_stus=Off +1_stus=Heating +2_stus=Cooling +UNDEF_fstus=- +NULL_fstus=- +-_fstus=- +0_fstus=Off +1_fstus=On +UNDEF_mode=- +NULL_mode=- +-_mode=- +0_mode=Off +1_mode=Heat +2_mode=Cool +3_mode=Auto +UNDEF_fan=- +NULL_fan=- +-_fan=- +0_fan=Auto +1_fan=Auto/Circulate +2_fan=On +UNDEF_pgm=- +NULL_pgm=- +-_pgm=- +-1_pgm=None +0_pgm=Program A +1_pgm=Program B +2_pgm=Vacation +3_pgm=Holiday +UNDEF_over=- +NULL_over=- +-_over=- +0_over=No +1_over=Yes + +``` + +radiotherm.things: + +```java +radiothermostat:rtherm:mytherm1 "My 1st floor thermostat" [ hostName="192.168.10.1", refresh=2, logRefresh=10, isCT80=false, disableLogs=false ] +radiothermostat:rtherm:mytherm2 "My 2nd floor thermostat" [ hostName="mythermhost2", refresh=1, logRefresh=20, isCT80=true, disableLogs=false ] +``` + +radiotherm.items: + +```java +Number:Temperature Therm_Temp "Current Temperature [%.1f °F] " { channel="radiothermostat:rtherm:mytherm1:temperature" } +// Humidity only supported on CT80 +Number Therm_Hum "Current Humidity [%d %%]" { channel="radiothermostat:rtherm:mytherm1:humidity" } +Number Therm_Mode "Thermostat Mode [MAP(radiotherm.map):%s_mode]" { channel="radiothermostat:rtherm:mytherm1:mode" } +// The Auto/Circulate option will only appear for CT80 +Number Therm_Fmode "Fan Mode [MAP(radiotherm.map):%s_fan]" { channel="radiothermostat:rtherm:mytherm1:fan_mode" } +// Program Mode only supported on CT80 Rev B +Number Therm_Pmode "Program Mode [MAP(radiotherm.map):%s_pgm]" { channel="radiothermostat:rtherm:mytherm1:program_mode" } +Number:Temperature Therm_Setpt "Set Point [%d]" { channel="radiothermostat:rtherm:mytherm1:set_point" } +Number Therm_Status "Status [MAP(radiotherm.map):%s_stus]" { channel="radiothermostat:rtherm:mytherm1:status" } +Number Therm_FanStatus "Fan Status [MAP(radiotherm.map):%s_fstus]" { channel="radiothermostat:rtherm:mytherm1:fan_status" } +Number Therm_Override "Override [MAP(radiotherm.map):%s_over]" { channel="radiothermostat:rtherm:mytherm1:override" } +Switch Therm_Hold "Hold" { channel="radiothermostat:rtherm:mytherm1:hold" } + +Number Therm_Day "Thermostat Day [%s]" { channel="radiothermostat:rtherm:mytherm1:day" } +Number Therm_Hour "Thermostat Hour [%s]" { channel="radiothermostat:rtherm:mytherm1:hour" } +Number Therm_Minute "Thermostat Minute [%s]" { channel="radiothermostat:rtherm:mytherm1:minute" } +String Therm_Dstmp "Thermostat DateStamp [%s]" + + org.openhab.addons.bundles + org.openhab.binding.heliosventilation + ${project.version} + org.openhab.addons.bundles org.openhab.binding.heos diff --git a/bundles/org.openhab.binding.heliosventilation/.classpath b/bundles/org.openhab.binding.heliosventilation/.classpath new file mode 100644 index 0000000000000..d575057b37021 --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.heliosventilation/.project b/bundles/org.openhab.binding.heliosventilation/.project new file mode 100644 index 0000000000000..a30a440ea2061 --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.heliosventilation + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.heliosventilation/NOTICE b/bundles/org.openhab.binding.heliosventilation/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.heliosventilation/README.md b/bundles/org.openhab.binding.heliosventilation/README.md new file mode 100644 index 0000000000000..7eafc19d8408d --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/README.md @@ -0,0 +1,111 @@ +# HeliosVentilation Binding + +This is the binding for Helios Ventilation Systems KWL EC 200/300/500 Pro. +It requires a connection to the RS485 bus used by the original remote controls KWL-FB (9417) and does not use the Modbus/TCP interface of the newer EasyControl devices. + +For electrical connection it is recommended to use an USB-RS485 interface, but any RS485 interface that shows up as a serial port will do. +Setup the device as described in https://www.openhab.org/docs/administration/serial.html. + +The binding will use the remote control address 15 for communication, so make sure that this is not assigned to a physically present remote control. + +## Supported Things + +There is only one thing type supported by this binding: a Helios Ventilation System KWL EC 200/300/500 Pro from Helios. +The binding was developed and test on a KWL EC 200 Pro device. + +## Binding Configuration + +The binding requires access to the serial device connecting to the RS485 bus as described in https://www.openhab.org/docs/administration/serial.html. +Otherwise only thing configuration is needed. + +## Thing Configuration + +The binding supports only one thing and requires the configuration of the serial port (typically /dev/ttyUSB0 on Linux and COM3 on Windows) and optionally the polling time which is the cycle time after which the binding tries to reconnect to the bus and requests data updates. + +## Channels + +Supported operation channels: + +| channel | type | description | +|--------------------|----------------------|-----------------------------------------------| +| outsideTemp | Number:Temperature | Temperature sensor in the outside air flow | +| outgoingTemp | Number:Temperature | Temperature sensor in the outgoing air flow | +| extractTemp | Number:Temperature | Temperature sensor in the extract air flow | +| supplyTemp | Number:Temperature | Temperature sensor in the supply air flow | +| setTemp | Number:Temperature | Set temperature for supply (not always used) | +| fanspeed | Number | Level of the fanspeed (1-8) | +| powerState | Switch | Main power switch | +| co2State | Switch | Switch for CO2 regulation | +| rhState | Switch | Switch for humidity regulation | +| winterMode | Switch | Switch to set winter mode | + +Supported configuration channels: + +| channel | type | description | +|--------------------|----------------------|-----------------------------------------------| +| bypassTemp | Number:Temperature | Temperature to disable the bypass function | +| supplyStopTemp | Number:Temperature | Temperature to stop supply fan for defrosting | +| preheatTemp | Number:Temperature | Temperature to enable the preheater | +| minFanspeed | Number | Minimal level of the fanspeed (1-8) | +| maxFanspeed | Number | Maximal level of the fanspeed (1-8) | +| rhLimit | Number:Dimensionless | Limit for relative humidity sensor | +| hysteresis | Number:Temperature | Hysteresis on defroster temperature | +| DCFanExtract | Number:Dimensionless | Speed reduction for the extract fan | +| DCFanSupply | Number:Dimensionless | Speed reduction for the supply fan | +| maintenanceInterval| Number:Dimensionless | Maintenance interval in months | +| adjustInveral | Number:Dimensionless | Adjust interval in minutes for air quality | +| RHLevelAuto | Switch | Automatic base humidity determination | +| switchType | Switch | External Switch type (Boost or Fireplace) | +| radiatorType | Switch | Use water (ON) or electric (OFF) radiator | +| cascade | Switch | System is cascaded | + +Note: the configuration channels are not intended to be written regularly. + +## Full Example + +Things: + +``` +heliosventilation:ventilation:MyKWL [ serialPort="/dev/ttyUSB0" ] +``` + +Items: + +``` +Switch KWLOnOff { channel="heliosventilation:ventilation:MyKWL:powerState" } +Switch KWLWinter { channel="heliosventilation:ventilation:MyKWL:winterMode" } + +Group VentilationTemp "Measured Temperatures in Ventilation System" + +Number:Temperature Outside_Temperature "Outside Temperature [%.1f °C]" (VentilationTemp) { channel="heliosventilation:ventilation:MyKWL:outsideTemp" } +Number:Temperature Outgoing_Temperature "Outgoing Temperature [%.1f °C]" (VentilationTemp) { channel="heliosventilation:ventilation:MyKWL:outgoingTemp" } +Number:Temperature Extract_Temperature "Extract Temperature [%.1f °C]" (VentilationTemp) { channel="heliosventilation:ventilation:MyKWL:extractTemp" } +Number:Temperature Supply_Temperature "Supply Temperature [%.1f °C]" (VentilationTemp) { channel="heliosventilation:ventilation:MyKWL:supplyTemp" } + +Number Fan_Speed "Fan Speed" { channel="heliosventilation:ventilation:MyKWL:fanspeed" } +Number Min_Fan_Speed "Min Fan Speed" { channel="heliosventilation:ventilation:MyKWL:minFanspeed" } +Number Max_Fan_Speed "Max Fan Speed" { channel="heliosventilation:ventilation:MyKWL:maxFanspeed" } + +``` + +Sitemap: + +``` +sitemap helios_kwl label="Helios Ventilation" { + Frame label="Temperatures" { + Text item=Outside_Temperature + Text item=Outgoing_Temperature + Text item=Extract_Temperature + Text item=Supply_Temperature + } + Frame label="Control" { + Switch item=KWLOnOff + Switch item=KWLWinter + Slider item=Fan_Speed icon="fan" minValue=1 maxValue=8 step=1 + } + Frame label="Configuration" { + Slider item=Min_Fan_Speed + Setpoint item=Max_Fan_Speed icon="fan" + } +} +``` diff --git a/bundles/org.openhab.binding.heliosventilation/pom.xml b/bundles/org.openhab.binding.heliosventilation/pom.xml new file mode 100644 index 0000000000000..85cb9ed2b8622 --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.7-SNAPSHOT + + + org.openhab.binding.heliosventilation + + openHAB Add-ons :: Bundles :: HeliosVentilation Binding + + diff --git a/bundles/org.openhab.binding.heliosventilation/src/main/feature/feature.xml b/bundles/org.openhab.binding.heliosventilation/src/main/feature/feature.xml new file mode 100644 index 0000000000000..079e81596decf --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + openhab-transport-serial + mvn:org.openhab.addons.bundles/org.openhab.binding.heliosventilation/${project.version} + + diff --git a/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosPropertiesFormatException.java b/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosPropertiesFormatException.java new file mode 100644 index 0000000000000..199c11cea7c5e --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosPropertiesFormatException.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.heliosventilation.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link HeliosPropertiesFormatException} class defines an exception to describe parsing format errors + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +public class HeliosPropertiesFormatException extends Exception { + private static final long serialVersionUID = 8051109351111509577L; + private final String channelName; + private final String fullSpec; + private final String reason; + + public HeliosPropertiesFormatException(String reason, String channelName, String fullSpec) { + this.channelName = channelName; + this.fullSpec = fullSpec; + this.reason = reason; + } + + public String getChannelName() { + return channelName; + } + + public String getFullSpec() { + return fullSpec; + } + + @Override + public String getMessage() { + return "Cannot parse '" + fullSpec + "' for datapoint '" + channelName + "': " + reason; + } +} diff --git a/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationBindingConstants.java b/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationBindingConstants.java new file mode 100644 index 0000000000000..129ec4b8a6fac --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationBindingConstants.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.heliosventilation.internal; + +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link HeliosVentilationBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +public class HeliosVentilationBindingConstants { + + public static final String BINDING_ID = "heliosventilation"; + + public static final String DATAPOINT_FILE = "datapoints.properties"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_HELIOS_VENTILATION = new ThingTypeUID(BINDING_ID, "ventilation"); + + public static final Map DATAPOINTS; + + private static final Logger LOGGER; + static { + /* logger is used by readChannelProperties() so we need to initialize logger first. */ + LOGGER = LoggerFactory.getLogger(HeliosVentilationBindingConstants.class); + DATAPOINTS = readChannelProperties(); + } + // List of all Channel ids + // Channel ids are only in datapoints.properties and thing-types.xml + + /** + * parse datapoints from properties + * + */ + private static Map readChannelProperties() { + HashMap result = new HashMap(); + + URL resource = Thread.currentThread().getContextClassLoader().getResource(DATAPOINT_FILE); + Properties properties = new Properties(); + try { + properties.load(resource.openStream()); + + Enumeration keys = properties.keys(); + while (keys.hasMoreElements()) { + String channel = (String) keys.nextElement(); + HeliosVentilationDataPoint dp; + try { + dp = new HeliosVentilationDataPoint(channel, properties.getProperty(channel)); + if (result.containsKey(dp.address())) { + result.get(dp.address()).append(dp); + } else { + result.put(dp.address(), dp); + } + } catch (HeliosPropertiesFormatException e) { + LOGGER.warn("could not read resource file {}, binding will probably fail: {}", DATAPOINT_FILE, + e.getMessage()); + } + } + } catch (IOException e) { + LOGGER.warn("could not read resource file {}, binding will probably fail: {}", DATAPOINT_FILE, + e.getMessage()); + } + + return result; + } + +} diff --git a/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationConfiguration.java b/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationConfiguration.java new file mode 100644 index 0000000000000..be054b8fd2122 --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationConfiguration.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.heliosventilation.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link HeliosVentilationConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +public class HeliosVentilationConfiguration { + + /** + * Port name for a serial connection to RS485 bus. Valid values are e.g. COM1 for Windows and /dev/ttyS0 or + * /dev/ttyUSB0 for Linux. + */ + public String serialPort = ""; + + /** + * The Panel Poll Period. Default is 60 sec. = 1 minute; + */ + public int pollPeriod = 60; + +} diff --git a/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationDataPoint.java b/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationDataPoint.java new file mode 100644 index 0000000000000..26c720ed27a3d --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationDataPoint.java @@ -0,0 +1,334 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.heliosventilation.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; + +/** + * The {@next HeliosVentilationDataPoint} is a description of a datapoint in the Helios ventilation system. + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +public class HeliosVentilationDataPoint { + public enum DataType { + TEMPERATURE, + HYSTERESIS, + FANSPEED, + SWITCH, + BYTE_PERCENT, + PERCENT, + NUMBER + } + + /** + * mapping from temperature byte values to °C + */ + private static final int[] TEMP_MAP = { -74, -70, -66, -62, -59, -56, -54, -52, -50, -48, -47, -46, -44, -43, -42, + -41, -40, -39, -38, -37, -36, -35, -34, -33, -33, -32, -31, -30, -30, -29, -28, -28, -27, -27, -26, -25, + -25, -24, -24, -23, -23, -22, -22, -21, -21, -20, -20, -19, -19, -19, -18, -18, -17, -17, -16, -16, -16, + -15, -15, -14, -14, -14, -13, -13, -12, -12, -12, -11, -11, -11, -10, -10, -9, -9, -9, -8, -8, -8, -7, -7, + -7, -6, -6, -6, -5, -5, -5, -4, -4, -4, -3, -3, -3, -2, -2, -2, -1, -1, -1, -1, 0, 0, 0, 1, 1, 1, 2, 2, 2, + 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, + 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 21, 21, 21, 22, 22, 22, + 23, 23, 24, 24, 24, 25, 25, 26, 26, 27, 27, 27, 28, 28, 29, 29, 30, 30, 31, 31, 32, 32, 33, 33, 34, 34, 35, + 35, 36, 36, 37, 37, 38, 38, 39, 40, 40, 41, 41, 42, 43, 43, 44, 45, 45, 46, 47, 48, 48, 49, 50, 51, 52, 53, + 53, 54, 55, 56, 57, 59, 60, 61, 62, 63, 65, 66, 68, 69, 71, 73, 75, 77, 79, 81, 82, 86, 90, 93, 97, 100, + 100, 100, 100, 100, 100, 100, 100, 100 }; + + /** + * mapping from human readable fanspeed to raw value + */ + private static final int[] FANSPEED_MAP = { 0, 1, 3, 7, 15, 31, 63, 127, 255 }; + + private static final int BYTE_PERCENT_OFFSET = 52; + + private String name; + private boolean writable; + private DataType datatype; + private byte address; + private int bitStart; + private int bitLength; + + private @Nullable HeliosVentilationDataPoint next; + + /** + * parse fullSpec in the properties format to declare a datapoint + * + * @param name the name of the datapoint + * @param fullSpec datapoint specification, see format in datapoints.properties + * @throws HeliosPropertiesFormatException in case fullSpec is not parsable + */ + public HeliosVentilationDataPoint(String name, String fullSpec) throws HeliosPropertiesFormatException { + String specWithoutComment; + if (fullSpec.contains("#")) { + specWithoutComment = fullSpec.substring(0, fullSpec.indexOf("#")); + } else { + specWithoutComment = fullSpec; + } + String[] tokens = specWithoutComment.split(","); + this.name = name; + if (tokens.length != 3) { + throw new HeliosPropertiesFormatException("invalid length", name, fullSpec); + } + try { + String addr = tokens[0]; + String[] addrTokens; + if (addr.contains(":")) { + addrTokens = addr.split(":"); + } else { + addrTokens = new String[] { addr }; + } + bitLength = 8; + bitStart = 0; + this.address = (byte) (int) Integer.decode(addrTokens[0]); + if (addrTokens.length > 1) { + bitStart = (byte) (int) Integer.decode(addrTokens[1]); + bitLength = 1; + } + if (addrTokens.length > 2) { + bitLength = (byte) (int) Integer.decode(addrTokens[2]) - bitStart + 1; + } + if (addrTokens.length > 3) { + throw new HeliosPropertiesFormatException( + "invalid address spec: too many separators in bit specification", name, fullSpec); + } + } catch (NumberFormatException e) { + throw new HeliosPropertiesFormatException("invalid address spec", name, fullSpec); + } + + this.writable = Boolean.parseBoolean(tokens[1]); + try { + this.datatype = DataType.valueOf(tokens[2].replaceAll("\\s+", "")); + } catch (IllegalArgumentException e) { + throw new HeliosPropertiesFormatException("invalid type spec", name, fullSpec); + } + } + + public HeliosVentilationDataPoint(String name, byte address, boolean writable, DataType datatype) { + this.datatype = datatype; + this.writable = writable; + this.name = name; + this.address = address; + } + + public boolean isWritable() { + return writable; + } + + @Override + public String toString() { + return name; + } + + /** + * + * @return the name of the variable, which is also the channel name + */ + public String getName() { + return name; + } + + /** + * + * @return address of the variable + */ + public byte address() { + return address; + } + + /** + * @return the bit mask of the data point. 0xFF in case the full byte is used. + */ + public byte bitMask() { + byte mask = (byte) 0xff; + if (datatype == DataType.NUMBER || datatype == DataType.SWITCH) { + mask = (byte) (((1 << bitLength) - 1) << bitStart); + } + return mask; + } + + /** + * interpret the given byte b and return the value as State. + * + * @param b + * @return state representation of byte value b in current datatype + */ + public State asState(byte b) { + int val = b & 0xff; + switch (datatype) { + case TEMPERATURE: + return new QuantityType<>(TEMP_MAP[val], SIUnits.CELSIUS); + case BYTE_PERCENT: + return new QuantityType<>((int) ((val - BYTE_PERCENT_OFFSET) * 100.0 / (255 - BYTE_PERCENT_OFFSET)), + SmartHomeUnits.PERCENT); + case SWITCH: + if (bitLength != 1) { + return UnDefType.UNDEF; + } else if ((b & (1 << bitStart)) != 0) { + return OnOffType.ON; + } else { + return OnOffType.OFF; + } + case NUMBER: + int value = (b & bitMask()) >> bitStart; + return new DecimalType(value); + case PERCENT: + return new QuantityType<>(val, SmartHomeUnits.PERCENT); + case FANSPEED: + int i = 1; + while (i < FANSPEED_MAP.length && FANSPEED_MAP[i] < val) { + i++; + } + return new DecimalType(i); + case HYSTERESIS: + return new QuantityType<>(val / 3, SIUnits.CELSIUS); + default: + return UnDefType.UNDEF; + } + } + + /** + * interpret the given byte b and return the value as string. + * + * @param b + * @return sting representation of byte value b in current datatype + */ + public String asString(byte b) { + State ste = asState(b); + String str = ste.toString(); + if (ste instanceof UnDefType) { + return String.format(" %02X ", b); + } else { + return str; + } + } + + /** + * generate byte data to transmit + * + * @param val is the state of a channel + * @return byte value with RS485 representation. Bit level values are returned in the correct location, but other + * bits/datapoints in the same address are zero. + */ + public byte getTransmitDataFor(State val) { + byte result = 0; + DecimalType value = val.as(DecimalType.class); + if (value == null) { + /* + * if value is not convertible to a numeric type we cannot do anything reasonable with it, let's use the + * initial value for it + */ + } else { + QuantityType quantvalue; + switch (datatype) { + case TEMPERATURE: + quantvalue = ((QuantityType) val); + quantvalue = quantvalue.toUnit(SIUnits.CELSIUS); + if (quantvalue != null) { + value = quantvalue.as(DecimalType.class); + if (value != null) { + int temp = (int) Math.round(value.doubleValue()); + int i = 0; + while (i < TEMP_MAP.length && TEMP_MAP[i] < temp) { + i++; + } + result = (byte) i; + } + } + break; + case FANSPEED: + int i = value.intValue(); + if (i < 0) { + i = 0; + } else if (i > 8) { + i = 8; + } + result = (byte) FANSPEED_MAP[i]; + break; + case BYTE_PERCENT: + result = (byte) ((value.doubleValue() / 100.0) * (255 - BYTE_PERCENT_OFFSET) + BYTE_PERCENT_OFFSET); + break; + case PERCENT: + double d = (Math.round(value.doubleValue())); + if (d < 0.0) { + d = 0.0; + } else if (d > 100.0) { + d = 100.0; + } + result = (byte) d; + break; + case HYSTERESIS: + quantvalue = ((QuantityType) val).toUnit(SIUnits.CELSIUS); + if (quantvalue != null) { + result = (byte) (quantvalue.intValue() * 3); + } + break; + case SWITCH: + case NUMBER: + // those are the types supporting bit level specification + // output only the relevant bits + result = (byte) (value.intValue() << bitStart); + break; + } + } + + return result; + } + + /** + * Get further datapoint linked to the same address. + * + * @return sister datapoint + */ + public @Nullable HeliosVentilationDataPoint next() { + return next; + } + + /** + * Add a next to a datapoint on the same address. + * Caller has to ensure that identical datapoints are not added several times. + * + * @param next is the sister datapoint + */ + public void append(HeliosVentilationDataPoint next) { + HeliosVentilationDataPoint existing = this.next; + if (this == next) { + // this datapoint is already there, so we do nothing and return + return; + } else if (existing != null) { + existing.append(next); + } else { + this.next = next; + } + } + + /** + * @return true if writing to this datapoint requires a read-modify-write on the address + */ + public boolean requiresReadModifyWrite() { + /* + * the address either has multiple datapoints linked to it or is a bit-level point + * this means we need to do read-modify-write on udpate and therefore we store the data in memory + */ + return (bitMask() != (byte) 0xFF || next != null); + } +} diff --git a/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationHandler.java b/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationHandler.java new file mode 100644 index 0000000000000..c870b18670240 --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationHandler.java @@ -0,0 +1,457 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.heliosventilation.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.TooManyListenersException; +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.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.io.transport.serial.PortInUseException; +import org.eclipse.smarthome.io.transport.serial.SerialPort; +import org.eclipse.smarthome.io.transport.serial.SerialPortEvent; +import org.eclipse.smarthome.io.transport.serial.SerialPortEventListener; +import org.eclipse.smarthome.io.transport.serial.SerialPortIdentifier; +import org.eclipse.smarthome.io.transport.serial.SerialPortManager; +import org.eclipse.smarthome.io.transport.serial.UnsupportedCommOperationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link HeliosVentilationHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +public class HeliosVentilationHandler extends BaseThingHandler implements SerialPortEventListener { + private static final int BUSMEMBER_MAINBOARD = 0x11; + private static final int BUSMEMBER_SLAVEBOARDS = 0x10; + private static final byte BUSMEMBER_CONTROLBOARDS = (byte) 0x20; + private static final int BUSMEMBER_REC_MASK = 0xF0; // interpreting frames delivered to BUSMEMBER_ME & + // BUSMEMBER_REC_MASK + private static final int BUSMEMBER_ME = 0x2F; // used as sender when communicating with the helios system + private static final int POLL_OFFLINE_THRESHOLD = 3; + + /** Logger Instance */ + private final Logger logger = LoggerFactory.getLogger(HeliosVentilationHandler.class); + + /** + * store received data for read-modify-write operations on bitlevel + */ + private final Map memory = new HashMap(); + + private final SerialPortManager serialPortManager; + + /** + * init to default to avoid NPE in case handleCommand() is called before initialize() + */ + private HeliosVentilationConfiguration config = new HeliosVentilationConfiguration(); + + private @Nullable SerialPort serialPort; + private @Nullable InputStream inputStream; + private @Nullable OutputStream outputStream; + + private @Nullable ScheduledFuture pollingTask; + private int pollCounter; + + public HeliosVentilationHandler(Thing thing, final SerialPortManager serialPortManager) { + super(thing); + this.serialPortManager = serialPortManager; + } + + @Override + public void initialize() { + config = getConfigAs(HeliosVentilationConfiguration.class); + + logger.debug("Serial Port: {}, 9600 baud, PollPeriod: {}", config.serialPort, config.pollPeriod); + + if (config.serialPort.length() < 1) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set!"); + return; + } else { + SerialPortIdentifier portId = serialPortManager.getIdentifier(config.serialPort); + if (portId == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Port " + config.serialPort + " is not known!"); + serialPort = null; + } else { + updateStatus(ThingStatus.UNKNOWN); + if (this.config.pollPeriod > 0) { + startPolling(); + } + } + } + + scheduler.execute(this::connect); + } + + private synchronized void connect() { + logger.debug("HeliosVentilation: connecting..."); + // parse ports and if the port is found, initialize the reader + SerialPortIdentifier portId = serialPortManager.getIdentifier(config.serialPort); + if (portId == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Port " + config.serialPort + " is not known!"); + serialPort = null; + + disconnect(); + } else if (!isConnected()) { + // initialize serial port + try { + SerialPort serial = portId.open(getThing().getUID().toString(), 2000); + serial.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); + serial.addEventListener(this); + + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + // ignore the exception on close + inputStream = null; + } + try { + if (outputStream != null) { + outputStream.close(); + } + } catch (IOException e) { + // ignore the exception on close + outputStream = null; + } + + inputStream = serial.getInputStream(); + outputStream = serial.getOutputStream(); + + // activate the DATA_AVAILABLE notifier + serial.notifyOnDataAvailable(true); + serialPort = serial; + updateStatus(ThingStatus.UNKNOWN); + } catch (final IOException ex) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error!"); + } catch (PortInUseException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Port is in use!"); + } catch (TooManyListenersException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Cannot attach listener to port!"); + } catch (UnsupportedCommOperationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Serial port does not support the RS485 parameters of the Helios remote protocol."); + } + } + } + + @Override + public void dispose() { + stopPolling(); + disconnect(); + super.dispose(); + } + + /** + * Start the polling task. + */ + public synchronized void startPolling() { + final ScheduledFuture task = pollingTask; + if (task != null && task.isCancelled()) { + task.cancel(true); + } + if (config.pollPeriod > 0) { + pollingTask = scheduler.scheduleWithFixedDelay(this::polling, 10, config.pollPeriod, TimeUnit.SECONDS); + } else { + pollingTask = null; + } + } + + /** + * Stop the polling task. + */ + public synchronized void stopPolling() { + final ScheduledFuture task = pollingTask; + if (task != null && !task.isCancelled()) { + task.cancel(true); + pollingTask = null; + } + } + + /** + * Method for polling the RS485 Helios RemoteContol bus + */ + public synchronized void polling() { + if (logger.isTraceEnabled()) { + logger.trace("HeliosVentilation Polling data for '{}'", getThing().getUID()); + } + pollCounter++; + if (pollCounter > POLL_OFFLINE_THRESHOLD) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.GONE, "No data received!"); + logger.info("No data received for '{}' disconnecting now...", getThing().getUID()); + disconnect(); + } + + if (!isConnected()) { + connect(); // let's try to reconnect if the connection failed or was never established before + } + + HeliosVentilationBindingConstants.DATAPOINTS.values().forEach((v) -> { + if (isLinked(v.getName())) { + poll(v); + } + }); + } + + private void disconnect() { + if (thing.getStatus() != ThingStatus.REMOVING) { + updateStatus(ThingStatus.OFFLINE); + } + synchronized (this) { + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + // ignore the exception on close + inputStream = null; + } + try { + if (outputStream != null) { + outputStream.close(); + } + } catch (IOException e) { + // ignore the exception on close + outputStream = null; + } + + SerialPort serial = serialPort; + if (serial != null) { + serial.close(); + } + serialPort = null; + } + + } + + private void poll(HeliosVentilationDataPoint v) { + byte[] txFrame = { 0x01, BUSMEMBER_ME, BUSMEMBER_MAINBOARD, 0x00, v.address(), 0x00 }; + txFrame[5] = (byte) checksum(txFrame); + + tx(txFrame); + } + + /* + * transmit a frame + */ + private void tx(byte[] txFrame) { + try { + OutputStream out = outputStream; + if (out != null) { + if (logger.isTraceEnabled()) { + logger.trace("HeliosVentilation: Write to serial port: {}", + String.format("%02x %02x %02x %02x", txFrame[1], txFrame[2], txFrame[3], txFrame[4])); + } + + out.write(txFrame); + out.flush(); + // after each frame we have to wait. + // 30 ms is taken from what we roughly see the original remote control is doing + Thread.sleep(30); + } + } catch (IOException e) { + // in case we cannot write the connection is somehow broken, let's officially disconnect + disconnect(); + connect(); + } catch (InterruptedException e) { + // ignore if we got interrupted + } + } + + /** + * Check connection status + * + * @return true if currently connected + */ + private boolean isConnected() { + return serialPort != null && inputStream != null && outputStream != null; + } + + @Override + public synchronized void serialEvent(SerialPortEvent event) { + switch (event.getEventType()) { + case SerialPortEvent.DATA_AVAILABLE: + // we get here if data has been received + + try { + // Wait roughly a frame length to ensure that the complete frame is already buffered. This improves + // the robustness for RS485/USB converters which sometimes duplicate bytes otherwise. + Thread.sleep(8); + } catch (InterruptedException e) { + // ignore interruption + } + + byte[] frame = { 0, 0, 0, 0, 0, 0 }; + InputStream in = inputStream; + if (in != null) { + try { + do { + int cnt = 0; + // read data from serial device + while (cnt < 6 && in.available() > 0) { + final int bytes = in.read(frame, cnt, 1); + if (cnt > 0 || frame[0] == 0x01) { + // only proceed if the first byte was 0x01 + cnt += bytes; + } + } + int sum = checksum(frame); + if (sum == (frame[5] & 0xff)) { + if (logger.isTraceEnabled()) { + logger.trace("HeliosVentilation: Read from serial port: {}", String + .format("%02x %02x %02x %02x", frame[1], frame[2], frame[3], frame[4])); + } + interpretFrame(frame); + + } else { + if (logger.isTraceEnabled()) { + logger.trace( + "HeliosVentilation: Read frame with not matching checksum from serial port: {}", + String.format("%02x %02x %02x %02x %02x %02x (expected %02x)", frame[0], + frame[1], frame[2], frame[3], frame[4], frame[5], sum)); + } + + } + + } while (in.available() > 0); + + } catch (IOException e1) { + logger.debug("Error reading from serial port: {}", e1.getMessage(), e1); + } + } + break; + default: + break; + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + scheduler.execute(this::polling); + } else if (command instanceof DecimalType || command instanceof QuantityType || command instanceof OnOffType) { + scheduler.execute(() -> update(channelUID, command)); + } + } + + /** + * Update the variable corresponding to given channel/command + * + * @param channelUID UID of the channel to update + * @param command data element to write + * + */ + public void update(ChannelUID channelUID, Command command) { + HeliosVentilationBindingConstants.DATAPOINTS.values().forEach((outer) -> { + HeliosVentilationDataPoint v = outer; + do { + if (channelUID.getThingUID().equals(thing.getUID()) && v.getName().equals(channelUID.getId())) { + if (v.isWritable()) { + byte[] txFrame = { 0x01, BUSMEMBER_ME, BUSMEMBER_CONTROLBOARDS, v.address(), 0x00, 0x00 }; + txFrame[4] = v.getTransmitDataFor((State) command); + if (v.requiresReadModifyWrite()) { + txFrame[4] |= memory.get(v.address()) & ~v.bitMask(); + memory.put(v.address(), txFrame[4]); + } + txFrame[5] = (byte) checksum(txFrame); + tx(txFrame); + + txFrame[2] = BUSMEMBER_SLAVEBOARDS; + txFrame[5] = (byte) checksum(txFrame); + tx(txFrame); + + txFrame[2] = BUSMEMBER_MAINBOARD; + txFrame[5] = (byte) checksum(txFrame); + tx(txFrame); + } + } + v = v.next(); + } while (v != null); + }); + + } + + /** + * calculate checksum of a frame + * + * @param frame filled with 5 bytes + * @return checksum of the first 5 bytes of frame + */ + private int checksum(byte[] frame) { + int sum = 0; + for (int a = 0; a < 5; a++) { + sum += frame[a] & 0xff; + } + sum %= 256; + return sum; + } + + /** + * interpret a frame, which is already validated to be in correct format with valid checksum + * + * @param frame 6 bytes long data with 0x01, sender, receiver, address, value, checksum + */ + private void interpretFrame(byte[] frame) { + if ((frame[2] & BUSMEMBER_REC_MASK) == (BUSMEMBER_ME & BUSMEMBER_REC_MASK)) { + // something to read for us + byte var = frame[3]; + byte val = frame[4]; + if (HeliosVentilationBindingConstants.DATAPOINTS.containsKey(var)) { + HeliosVentilationDataPoint datapoint = HeliosVentilationBindingConstants.DATAPOINTS.get(var); + if (datapoint.requiresReadModifyWrite()) { + memory.put(var, val); + } + do { + if (logger.isTraceEnabled()) { + String t = datapoint.asString(val); + logger.trace("Received {} = {}", datapoint, t); + } + updateStatus(ThingStatus.ONLINE); + pollCounter = 0; + + updateState(datapoint.getName(), datapoint.asState(val)); + datapoint = datapoint.next(); + } while (datapoint != null); + + } else { + if (logger.isTraceEnabled()) { + logger.trace("Received unkown data @{} = {}", String.format("%02X ", var), + String.format("%02X ", val)); + } + } + } + } +} diff --git a/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationHandlerFactory.java b/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationHandlerFactory.java new file mode 100644 index 0000000000000..9b5e4161652d1 --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/src/main/java/org/openhab/binding/heliosventilation/internal/HeliosVentilationHandlerFactory.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.heliosventilation.internal; + +import static org.openhab.binding.heliosventilation.internal.HeliosVentilationBindingConstants.THING_TYPE_HELIOS_VENTILATION; + +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.eclipse.smarthome.io.transport.serial.SerialPortManager; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link HeliosVentilationHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Raphael Mack - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.heliosventilation", service = ThingHandlerFactory.class) +public class HeliosVentilationHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections + .singleton(THING_TYPE_HELIOS_VENTILATION); + + private final SerialPortManager serialPortManager; + + @Activate + public HeliosVentilationHandlerFactory(@Reference SerialPortManager serialPortManager) { + this.serialPortManager = serialPortManager; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_HELIOS_VENTILATION.equals(thingTypeUID)) { + return new HeliosVentilationHandler(thing, serialPortManager); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.heliosventilation/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.heliosventilation/src/main/resources/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..a5a21351eabdc --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/src/main/resources/ESH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + HeliosVentilation Binding + This is the binding for Helios Ventilation Systems KWL EC 200/300/500 Pro. It requires a connection to the RS485 bus used by the original remote controls KWL-FB (9417). + Raphael Mack + + diff --git a/bundles/org.openhab.binding.heliosventilation/src/main/resources/ESH-INF/i18n/heliosventilation_de.properties b/bundles/org.openhab.binding.heliosventilation/src/main/resources/ESH-INF/i18n/heliosventilation_de.properties new file mode 100644 index 0000000000000..74c3293538f4c --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/src/main/resources/ESH-INF/i18n/heliosventilation_de.properties @@ -0,0 +1,85 @@ +# binding +binding.heliosventilation.name = Helios KWL Binding +binding.heliosventilation.description = Dies ist das Binding fr Helios KWL Systeme KWL EC 200/300/500 Pro. Es bentigt eine Verbindung zum RS485 Bus fr die Fernbedienung KWL-FB (9417). + +# thing types +thing-type.heliosventilation.ventilation.label = KWL +thing-type.heliosventilation.ventilation.description = Lftungsgert zur kontrollierten Wohnraumlftung. + +# thing type config description +thing-type.config.heliosventilation.ventilation.serialPort.label = Serielle Schnittstelle +thing-type.config.heliosventilation.ventilation.serialPort.description = Die Betreibssystembezeichnung des Gertes fr die serielle Schnittstelle. Gltige Werte sind z. B. COM1 unter Windows und /dev/ttyS0 oder /dev/ttyUSB0 unter GNU/Linux. +thing-type.config.heliosventilation.ventilation.pollPeriod.label = Poll-Zyklus +thing-type.config.heliosventilation.ventilation.pollPeriod.description = Der Poll-Zyklus in Sekunden, 0 fr keine wiederkehrende Aktualisierung. + +# channel types +channel-type.heliosventilation.outside_temperature.label = Auenlufttemperatur +channel-type.heliosventilation.outside_temperature.description = Temperatur gemessen im Auenluftstrom. + +channel-type.heliosventilation.outgoing_temperature.label = Fortlufttemperatur +channel-type.heliosventilation.outgoing_temperature.description = Temperatur gemessen im Fortluftstrom (das Haus verlassend). + +channel-type.heliosventilation.extract_temperature.label = Ablufttemperatur +channel-type.heliosventilation.extract_temperature.description = Temperatur gemessen im Abluftstrom (Raumluft). + +channel-type.heliosventilation.supply_temperature.label = Zulufttemperatur +channel-type.heliosventilation.supply_temperature.description = Temperatur gemessen im Zuluftstrom (in den Raum einstrhmend). + +channel-type.heliosventilation.bypass_temperature.label = WRG Bypasstemperatur +channel-type.heliosventilation.bypass_temperature.description = Temperaturwert der von der Auenlufttemperatur berschritten werden muss um im Sommerbetrieb die Wrmetauscherumgehung zu aktivieren. + +channel-type.heliosventilation.supply_stop_temperature.label = Frostschutztemperatur +channel-type.heliosventilation.supply_stop_temperature.description = Abschalttemperatur des Zuluftventilators zur Entfrostung des Wrmetauschers. + +channel-type.heliosventilation.preheat_temperature.label = Vorheizregister +channel-type.heliosventilation.preheat_temperature.description = Solltemperatur der Vorheizung fr die Entfrosterfunktion des Wrmetauschers. + +channel-type.heliosventilation.supply_stop_temperature.label = Frostschutztemperatur +channel-type.heliosventilation.supply_stop_temperature.description = Abschalttemperatur des Zuluftventilators zur Entfrostung des Wrmetauschers. + +channel-type.heliosventilation.preheat_temperature.label = Vorheizregister +channel-type.heliosventilation.preheat_temperature.description = Solltemperatur der Vorheizung fr die Entforsterfunktion des Wrmetauschers. + +channel-type.heliosventilation.fanspeed.label = Ventilator-Drehzahlstufe +channel-type.heliosventilation.min_fanspeed.label = Grundlftungsstufe +channel-type.heliosventilation.max_fanspeed.label = Maximale Drehzahlstufe + +channel-type.heliosventilation.rh_limit.label = Feuchtegrenzwert + +channel-type.heliosventilation.hysteresis.label = Entfrosterhysterese +channel-type.heliosventilation.hysteresis.description = Hysterese der Entfrosterfunktion + +channel-type.heliosventilation.set_temperature.label = Vorgabetemperatur +channel-type.heliosventilation.set_temperature.description = Vorgabe fr die Raumtemperatur. Wird nicht von allen Helios KWLs verwendet. + +channel-type.heliosventilation.dc_fan_extract.label = Ablufventilator +channel-type.heliosventilation.dc_fan_extract.description = Drehzahlreduktion des Abluftventilators. + +channel-type.heliosventilation.dc_fan_supply.label = Zulufventilator +channel-type.heliosventilation.dc_fan_supply.description = Drehzahlreduktion des Zuluftventilators. + +channel-type.heliosventilation.maintenance_interval.label = Wartungsinterval + +channel-type.heliosventilation.radiator_type.label = Wasser-Nachheizregister +channel-type.heliosventilation.radiator_type.description = An fr wasserbetriebenes, aus fr elektrisches Nachheizregister. + +channel-type.heliosventilation.switch_type.label = Stolftungstaste +channel-type.heliosventilation.switch_type.description = An: Externer Taster wird als Stolftungstaste verwendet. Aus: Taste ist in Funktion "Kamintaste". + +channel-type.heliosventilation.cascade_mode.label = Kaskadensteuerung + +channel-type.heliosventilation.rh_level_auto.label = Automatische Basisfeuchtebestimmung + +channel-type.heliosventilation.power_state.label = Hauptschalter +channel-type.heliosventilation.power_state.description = Schaltet die KWL an/aus. + +channel-type.heliosventilation.co2_state.label = CO2-Regelung +channel-type.heliosventilation.co2_state.description = Schaltet die CO2-Sensor-basierte Regelung an/aus. + +channel-type.heliosventilation.rh_state.label = Feuchteregelung +channel-type.heliosventilation.rh_state.description = Schaltet die Feuchte-basierte Regelung an/aus. + +channel-type.heliosventilation.winter_state.label = Winterbetrieb +channel-type.heliosventilation.winter_state.description = Schaltet die KWL in Winterbetrieb um die Bypass-Funktion zu deaktivieren. Falls Winterbetrieb aus ist, wird zur Khlung der Wrmetauscher umgangen, falls die Auenluft klter als die Abluft ist. + +channel-type.heliosventilation.adjust_interval.label = Regelintervall diff --git a/bundles/org.openhab.binding.heliosventilation/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.heliosventilation/src/main/resources/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..e3827b96aaba3 --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/src/main/resources/ESH-INF/thing/thing-types.xml @@ -0,0 +1,233 @@ + + + + + + + A domestic ventilation system (KWL) from Helios. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + serial-port + + The serial port name for the RS485 interfaces. Valid values are e.g. COM1 for Windows and /dev/ttyS0 or /dev/ttyUSB0 for Linux. + + + + + The poll period in seconds use 0 for no polling. + 60 + + + + + + Number:Temperature + + Temperature measured in the outdoor air flow. + Temperature + + + + + Number:Temperature + + Temperature measured in the extract (indoor, room temperature) air flow. + Temperature + + + + + Number:Temperature + + Temperature measured in the supply (incoming) air flow. + Temperature + + + + + Number:Temperature + + Temperature measured in the outgoing air flow. + Temperature + + + + + + Number:Temperature + + Set temperature for the supply air. Not used in all ventilation systems. + Temperature + + + + + Number + + HVAC + + + + + Number + + HVAC + + + + + Number + + HVAC + + + + + Number:Temperature + + Bypass temperature to disable the bypass function if outside temperature is below this threshold even if ventilation system is in summer mode. + Temperature + + + + + Number:Temperature + + Stop the supply fan if outside temperature is below this threshold. + Temperature + + + + + Number:Temperature + + Set temperature for preheater. + Temperature + + + + + Number:Dimensionless + + Limit for relative humidity sensor. + Humidity + + + + + Number:Dimensionless + + Speed of the supply air fan (incoming air). + HVAC + + + + + Number:Dimensionless + + Speed of the extract air fan (outgoing air). + HVAC + + + + + Number:Temperature + + Hysteresis on defroster temperature. + + + + + Switch + + State of the ventilation system. + + + + Switch + + Control the ventilation system by CO2 sensor. + + + + Switch + + Control the ventilation system by humidity sensor. + + + + Switch + + Ventilation system is in winter mode and will not use bypass for cooling. If OFF, the bypass function will be used for cooling if the outside temperature is above the Cell Bypass Temperature. + + + + Switch + + + + + Switch + + Ventilation system with water radiator (ON) or electric radiator (OFF). + + + + Switch + + External switch is used for boost (ON) or fireplace (OFF). + + + + Switch + + + + + Number:Dimensionless + + HVAC + + + + + Number:Dimensionless + + HVAC + + + + diff --git a/bundles/org.openhab.binding.heliosventilation/src/main/resources/datapoints.properties b/bundles/org.openhab.binding.heliosventilation/src/main/resources/datapoints.properties new file mode 100644 index 0000000000000..8a79a54a276d6 --- /dev/null +++ b/bundles/org.openhab.binding.heliosventilation/src/main/resources/datapoints.properties @@ -0,0 +1,48 @@ +# +# datapoints.properties - This file defines the datapoints of the Helios ventilation system +# +# Format: = ,writable,type +# +# bitspec is +# - a single digit in range 0-7 or +# - start:end (where start is the number of the LSB and end the number of the MSB of the field) +# +# type is one of +# - TEMPERATURE +# - FANSPEED +# - PERCENT +# - BYTE_PERCENT +# - SWITCH +# - NUMBER +# - HYSTERESIS +# +# on change of this file, ensure that the thing-types.xml is consistent + +fanspeed = 0x29,true,FANSPEED + +outsideTemp = 0x32,false,TEMPERATURE +outgoingTemp = 0x33,false,TEMPERATURE +extractTemp = 0x34,false,TEMPERATURE +supplyTemp = 0x35,false,TEMPERATURE + +DCFanSupply = 0xB0,true,PERCENT +DCFanExtract = 0xB1,true,PERCENT +hysteresis = 0xB2,true,HYSTERESIS +setTemp = 0xA4,true,TEMPERATURE +maxFanspeed = 0xA5,true,FANSPEED +maintenanceInterval = 0xA6:0:3,true,NUMBER +preheatTemp = 0xA7,true,TEMPERATURE +supplyStopTemp = 0xA8,true,TEMPERATURE +minFanspeed = 0xA9,true,FANSPEED +rhLimit = 0xAE,true,BYTE_PERCENT +bypassTemp = 0xAF,true,TEMPERATURE +adjustInveral = 0xAA:0:3,true,NUMBER +RHLevelAuto = 0xAA:4,true,SWITCH +switchType = 0xAA:5,true,SWITCH # ON = boost, OFF = fireplace +radiatorType = 0xAA:6,true,SWITCH # ON = water, OFF = electric +cascade = 0xAA:7,true,SWITCH + +powerState = 0xA3:0,true,SWITCH +co2State = 0xA3:1,true,SWITCH +rhState = 0xA3:2,true,SWITCH +winterMode = 0xA3:3,true,SWITCH # ON = bypass disabled diff --git a/bundles/pom.xml b/bundles/pom.xml index f86c3cbabee64..75860d95de272 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -107,6 +107,7 @@ org.openhab.binding.hdanywhere org.openhab.binding.hdpowerview org.openhab.binding.helios + org.openhab.binding.heliosventilation org.openhab.binding.heos org.openhab.binding.homematic org.openhab.binding.hpprinter From 0d57db5d919e3f26d6fdb3fc3acd4e1b99a90ec2 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Sun, 28 Jun 2020 08:40:42 +0200 Subject: [PATCH 11/85] [netatmo] Null annotations Part 2 of 3 (#8019) Related to #7913 Signed-off-by: Laurent Garnier --- .../netatmo/internal/WeatherUtils.java | 3 + .../handler/AbstractNetatmoThingHandler.java | 9 +- .../internal/handler/MeasurableChannels.java | 12 ++- .../handler/NetatmoBridgeHandler.java | 32 +++--- .../handler/NetatmoDeviceHandler.java | 102 ++++++++++-------- .../homecoach/NAHealthyHomeCoachHandler.java | 26 +++-- .../internal/station/NAMainHandler.java | 39 ++++--- .../internal/station/NAModule1Handler.java | 17 +-- .../internal/station/NAModule2Handler.java | 10 +- .../internal/station/NAModule3Handler.java | 20 ++-- .../internal/station/NAModule4Handler.java | 17 +-- .../internal/thermostat/NAPlugHandler.java | 29 +++-- .../internal/thermostat/NATherm1Handler.java | 63 ++++++----- .../webhook/WelcomeWebHookServlet.java | 18 +++- 14 files changed, 236 insertions(+), 161 deletions(-) diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/WeatherUtils.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/WeatherUtils.java index 4d04bff509011..32d1fce9b3e71 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/WeatherUtils.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/WeatherUtils.java @@ -12,12 +12,15 @@ */ package org.openhab.binding.netatmo.internal; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * This class holds various unit/measurement conversion methods * * @author Gaël L'hopital - Initial contribution * @author Rob Nielsen - updated heat index */ +@NonNullByDefault public class WeatherUtils { /** diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/AbstractNetatmoThingHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/AbstractNetatmoThingHandler.java index e6dc8117da4bf..aaa8e1d4fc47f 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/AbstractNetatmoThingHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/AbstractNetatmoThingHandler.java @@ -236,8 +236,13 @@ protected void updateProperties(Integer firmware, String modelId) { public void updateMeasurements() { } - public void getMeasurements(NetatmoBridgeHandler handler, String device, @Nullable String module, String scale, - List types, List channels, Map channelMeasurements) { + public void getMeasurements(String device, @Nullable String module, String scale, List types, + List channels, Map channelMeasurements) { + NetatmoBridgeHandler handler = getBridgeHandler(); + if (handler == null) { + return; + } + if (types.size() != channels.size()) { throw new IllegalArgumentException("types and channels lists are different sizes."); } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/MeasurableChannels.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/MeasurableChannels.java index 2ac60a287d918..b98055e715806 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/MeasurableChannels.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/MeasurableChannels.java @@ -18,6 +18,8 @@ import java.util.List; import java.util.Optional; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.types.State; import org.openhab.binding.netatmo.internal.ChannelTypeUtils; @@ -33,8 +35,9 @@ * @author Gaël L'hopital - Initial contribution * */ +@NonNullByDefault public class MeasurableChannels { - protected NAMeasureResponse measures; + protected @Nullable NAMeasureResponse measures; protected List measuredChannels = new ArrayList<>(); /* @@ -59,9 +62,10 @@ protected void removeChannel(ChannelUID channelUID) { protected Optional getNAThingProperty(String channelId) { int index = measuredChannels.indexOf(channelId); - if (index != -1 && measures != null) { - if (!measures.getBody().isEmpty()) { - List> valueList = measures.getBody().get(0).getValue(); + NAMeasureResponse theMeasures = measures; + if (index != -1 && theMeasures != null) { + if (!theMeasures.getBody().isEmpty()) { + List> valueList = theMeasures.getBody().get(0).getValue(); if (!valueList.isEmpty()) { List values = valueList.get(0); if (values.size() >= index) { diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoBridgeHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoBridgeHandler.java index 7c81f16b44b0e..da0fbc5868c8d 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoBridgeHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoBridgeHandler.java @@ -116,11 +116,11 @@ public void initialize() { private void connectionSucceed() { updateStatus(ThingStatus.ONLINE); - WelcomeWebHookServlet webHookServlet = this.webHookServlet; + WelcomeWebHookServlet servlet = webHookServlet; String webHookURI = getWebHookURI(); WelcomeApi welcomeApi = getWelcomeApi(); - if (welcomeApi != null && webHookServlet != null && webHookURI != null) { - webHookServlet.activate(this); + if (welcomeApi != null && servlet != null && webHookURI != null) { + servlet.activate(this); logger.debug("Setting up Netatmo Welcome WebHook"); welcomeApi.addwebhook(webHookURI, WEBHOOK_APP); } @@ -226,39 +226,39 @@ public void handleCommand(ChannelUID channelUID, Command command) { } public @Nullable PartnerApi getPartnerApi() { - APIMap apiMap = this.apiMap; - return apiMap != null ? (PartnerApi) apiMap.get(PartnerApi.class) : null; + APIMap map = apiMap; + return map != null ? (PartnerApi) map.get(PartnerApi.class) : null; } private @Nullable StationApi getStationApi() { - APIMap apiMap = this.apiMap; - return apiMap != null ? (StationApi) apiMap.get(StationApi.class) : null; + APIMap map = apiMap; + return map != null ? (StationApi) map.get(StationApi.class) : null; } private @Nullable HealthyhomecoachApi getHomeCoachApi() { - APIMap apiMap = this.apiMap; - return apiMap != null ? (HealthyhomecoachApi) apiMap.get(HealthyhomecoachApi.class) : null; + APIMap map = apiMap; + return map != null ? (HealthyhomecoachApi) map.get(HealthyhomecoachApi.class) : null; } public @Nullable ThermostatApi getThermostatApi() { - APIMap apiMap = this.apiMap; - return apiMap != null ? (ThermostatApi) apiMap.get(ThermostatApi.class) : null; + APIMap map = apiMap; + return map != null ? (ThermostatApi) map.get(ThermostatApi.class) : null; } public @Nullable WelcomeApi getWelcomeApi() { - APIMap apiMap = this.apiMap; - return apiMap != null ? (WelcomeApi) apiMap.get(WelcomeApi.class) : null; + APIMap map = apiMap; + return map != null ? (WelcomeApi) map.get(WelcomeApi.class) : null; } @Override public void dispose() { logger.debug("Running dispose()"); - WelcomeWebHookServlet webHookServlet = this.webHookServlet; + WelcomeWebHookServlet servlet = webHookServlet; WelcomeApi welcomeApi = getWelcomeApi(); - if (welcomeApi != null && webHookServlet != null && getWebHookURI() != null) { + if (welcomeApi != null && servlet != null && getWebHookURI() != null) { logger.debug("Releasing Netatmo Welcome WebHook"); - webHookServlet.deactivate(); + servlet.deactivate(); welcomeApi.dropwebhook(WEBHOOK_APP); } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoDeviceHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoDeviceHandler.java index ab31d0d807891..37f2ef5769fd8 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoDeviceHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoDeviceHandler.java @@ -23,7 +23,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.library.types.DecimalType; @@ -47,16 +47,16 @@ * * @author Gaël L'hopital - Initial contribution */ +@NonNullByDefault public abstract class NetatmoDeviceHandler extends AbstractNetatmoThingHandler { private static final int MIN_REFRESH_INTERVAL = 2000; private static final int DEFAULT_REFRESH_INTERVAL = 300000; - private Logger logger = LoggerFactory.getLogger(NetatmoDeviceHandler.class); - private ScheduledFuture refreshJob; - private RefreshStrategy refreshStrategy; - @Nullable - protected DEVICE device; + private final Logger logger = LoggerFactory.getLogger(NetatmoDeviceHandler.class); + private @Nullable ScheduledFuture refreshJob; + private @Nullable RefreshStrategy refreshStrategy; + protected @Nullable DEVICE device; protected Map childs = new ConcurrentHashMap<>(); public NetatmoDeviceHandler(Thing thing, final TimeZoneProvider timeZoneProvider) { @@ -71,13 +71,18 @@ protected void initializeThing() { } private void scheduleRefreshJob() { - long delay = refreshStrategy.nextRunDelayInS(); + RefreshStrategy strategy = refreshStrategy; + if (strategy == null) { + return; + } + long delay = strategy.nextRunDelayInS(); logger.debug("Scheduling update channel thread in {} s", delay); refreshJob = scheduler.schedule(() -> { updateChannels(); - if (refreshJob != null && !refreshJob.isCancelled()) { + ScheduledFuture job = refreshJob; + if (job != null) { logger.debug("cancel refresh job"); - refreshJob.cancel(false); + job.cancel(false); refreshJob = null; } scheduleRefreshJob(); @@ -87,9 +92,10 @@ private void scheduleRefreshJob() { @Override public void dispose() { logger.debug("Running dispose()"); - if (refreshJob != null && !refreshJob.isCancelled()) { + ScheduledFuture job = refreshJob; + if (job != null) { logger.debug("cancel refresh job"); - refreshJob.cancel(true); + job.cancel(true); refreshJob = null; } } @@ -101,12 +107,14 @@ protected void updateProperties(DEVICE deviceData) { @Override protected void updateChannels() { - if (refreshStrategy != null) { - logger.debug("Data aged of {} s", refreshStrategy.dataAge() / 1000); - if (refreshStrategy.isDataOutdated()) { + RefreshStrategy strategy = refreshStrategy; + if (strategy != null) { + logger.debug("Data aged of {} s", strategy.dataAge() / 1000); + if (strategy.isDataOutdated()) { logger.debug("Trying to update channels on device {}", getId()); childs.clear(); + @Nullable DEVICE newDeviceReading = null; try { newDeviceReading = updateReadings(); @@ -120,31 +128,33 @@ protected void updateChannels() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unable to connect Netatmo API : " + e.getLocalizedMessage()); } + NetatmoBridgeHandler bridgeHandler = getBridgeHandler(); if (newDeviceReading != null) { updateStatus(ThingStatus.ONLINE); logger.debug("Successfully updated device {} readings! Now updating channels", getId()); - DEVICE device = newDeviceReading; - this.device = device; - updateProperties(device); + DEVICE theDevice = newDeviceReading; + this.device = theDevice; + updateProperties(theDevice); Integer dataTimeStamp = getDataTimestamp(); if (dataTimeStamp != null) { - refreshStrategy.setDataTimeStamp(dataTimeStamp, timeZoneProvider.getTimeZone()); + strategy.setDataTimeStamp(dataTimeStamp, timeZoneProvider.getTimeZone()); } - radioHelper.ifPresent(helper -> helper.setModule(device)); - NetatmoBridgeHandler handler = getBridgeHandler(); - if (handler != null) { - handler.checkForNewThings(newDeviceReading); + radioHelper.ifPresent(helper -> helper.setModule(theDevice)); + if (bridgeHandler != null) { + bridgeHandler.checkForNewThings(newDeviceReading); } } else { logger.debug("Failed to update device {} readings! Skip updating channels", getId()); } // Be sure that all channels for the modules will be updated with refreshed data - childs.forEach((childId, moduleData) -> { - Optional childHandler = getBridgeHandler().findNAThing(childId); - childHandler.map(NetatmoModuleHandler.class::cast).ifPresent(naChildModule -> { - naChildModule.setRefreshRequired(true); + if (bridgeHandler != null) { + childs.forEach((childId, moduleData) -> { + Optional childHandler = bridgeHandler.findNAThing(childId); + childHandler.map(NetatmoModuleHandler.class::cast).ifPresent(naChildModule -> { + naChildModule.setRefreshRequired(true); + }); }); - }); + } } else { logger.debug("Data still valid for device {}", getId()); } @@ -154,21 +164,23 @@ protected void updateChannels() { } @Override - protected State getNAThingProperty(@NonNull String channelId) { + protected State getNAThingProperty(String channelId) { try { + @Nullable + DEVICE theDevice = device; switch (channelId) { case CHANNEL_LAST_STATUS_STORE: - if (device != null) { - Method getLastStatusStore = device.getClass().getMethod("getLastStatusStore"); - Integer lastStatusStore = (Integer) getLastStatusStore.invoke(device); + if (theDevice != null) { + Method getLastStatusStore = theDevice.getClass().getMethod("getLastStatusStore"); + Integer lastStatusStore = (Integer) getLastStatusStore.invoke(theDevice); return ChannelTypeUtils.toDateTimeType(lastStatusStore, timeZoneProvider.getTimeZone()); } else { return UnDefType.UNDEF; } case CHANNEL_LOCATION: - if (device != null) { - Method getPlace = device.getClass().getMethod("getPlace"); - NAPlace place = (NAPlace) getPlace.invoke(device); + if (theDevice != null) { + Method getPlace = theDevice.getClass().getMethod("getPlace"); + NAPlace place = (NAPlace) getPlace.invoke(theDevice); PointType point = new PointType(new DecimalType(place.getLocation().get(1)), new DecimalType(place.getLocation().get(0))); if (place.getAltitude() != null) { @@ -188,14 +200,17 @@ protected State getNAThingProperty(@NonNull String channelId) { } private void updateChildModules() { - logger.debug("Updating child modules of {}", getId()); - childs.forEach((childId, moduleData) -> { - Optional childHandler = getBridgeHandler().findNAThing(childId); - childHandler.map(NetatmoModuleHandler.class::cast).ifPresent(naChildModule -> { - logger.debug("Updating child module {}", naChildModule.getId()); - naChildModule.updateChannels(moduleData); + NetatmoBridgeHandler bridgeHandler = getBridgeHandler(); + if (bridgeHandler != null) { + logger.debug("Updating child modules of {}", getId()); + childs.forEach((childId, moduleData) -> { + Optional childHandler = bridgeHandler.findNAThing(childId); + childHandler.map(NetatmoModuleHandler.class::cast).ifPresent(naChildModule -> { + logger.debug("Updating child module {}", naChildModule.getId()); + naChildModule.updateChannels(moduleData); + }); }); - }); + } } /* @@ -231,6 +246,9 @@ private void defineRefreshInterval() { protected abstract @Nullable Integer getDataTimestamp(); public void expireData() { - refreshStrategy.expireData(); + RefreshStrategy strategy = refreshStrategy; + if (strategy != null) { + strategy.expireData(); + } } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/homecoach/NAHealthyHomeCoachHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/homecoach/NAHealthyHomeCoachHandler.java index ad28e769347d1..a8f78bd4aec59 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/homecoach/NAHealthyHomeCoachHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/homecoach/NAHealthyHomeCoachHandler.java @@ -15,11 +15,12 @@ import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.*; import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*; -import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler; import org.openhab.binding.netatmo.internal.handler.NetatmoDeviceHandler; import io.swagger.client.model.NADashboardData; @@ -32,16 +33,19 @@ * @author Michael Svinth - Initial contribution OH2 version * */ +@NonNullByDefault public class NAHealthyHomeCoachHandler extends NetatmoDeviceHandler { - public NAHealthyHomeCoachHandler(@NonNull Thing thing, final TimeZoneProvider timeZoneProvider) { + public NAHealthyHomeCoachHandler(Thing thing, final TimeZoneProvider timeZoneProvider) { super(thing, timeZoneProvider); } @Override - protected NAHealthyHomeCoach updateReadings() { + protected @Nullable NAHealthyHomeCoach updateReadings() { NAHealthyHomeCoach result = null; - NAHealthyHomeCoachDataBody homecoachDataBody = getBridgeHandler().getHomecoachDataBody(getId()); + NetatmoBridgeHandler bridgeHandler = getBridgeHandler(); + NAHealthyHomeCoachDataBody homecoachDataBody = bridgeHandler == null ? null + : bridgeHandler.getHomecoachDataBody(getId()); if (homecoachDataBody != null) { result = homecoachDataBody.getDevices().get(0); } @@ -54,9 +58,10 @@ protected void updateProperties(NAHealthyHomeCoach deviceData) { } @Override - protected State getNAThingProperty(@NonNull String channelId) { - if (device != null) { - NADashboardData dashboardData = device.getDashboardData(); + protected State getNAThingProperty(String channelId) { + NAHealthyHomeCoach healthyHomeCoachDevice = device; + if (healthyHomeCoachDevice != null) { + NADashboardData dashboardData = healthyHomeCoachDevice.getDashboardData(); switch (channelId) { case CHANNEL_CO2: return toQuantityType(dashboardData.getCO2(), API_CO2_UNIT); @@ -91,7 +96,7 @@ protected State getNAThingProperty(@NonNull String channelId) { return super.getNAThingProperty(channelId); } - private String toHealthIndexString(Integer healthIndex) { + private @Nullable String toHealthIndexString(@Nullable Integer healthIndex) { if (healthIndex == null) { return null; } @@ -113,8 +118,9 @@ private String toHealthIndexString(Integer healthIndex) { @Override protected @Nullable Integer getDataTimestamp() { - if (device != null) { - Integer lastStored = device.getLastStatusStore(); + NAHealthyHomeCoach healthyHomeCoachDevice = device; + if (healthyHomeCoachDevice != null) { + Integer lastStored = healthyHomeCoachDevice.getLastStatusStore(); if (lastStored != null) { return lastStored; } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAMainHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAMainHandler.java index 8f4643e2f3664..b0e07fb3c85f9 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAMainHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAMainHandler.java @@ -21,13 +21,14 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.types.State; import org.openhab.binding.netatmo.internal.WeatherUtils; import org.openhab.binding.netatmo.internal.handler.AbstractNetatmoThingHandler; +import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler; import org.openhab.binding.netatmo.internal.handler.NetatmoDeviceHandler; import org.openhab.binding.netatmo.internal.handler.NetatmoModuleHandler; @@ -43,6 +44,7 @@ * @author Rob Nielsen - Added day, week, and month measurements to the weather station and modules * */ +@NonNullByDefault public class NAMainHandler extends NetatmoDeviceHandler { private Map channelMeasurements = new ConcurrentHashMap<>(); @@ -51,9 +53,10 @@ public NAMainHandler(Thing thing, final TimeZoneProvider timeZoneProvider) { } @Override - protected NAMain updateReadings() { + protected @Nullable NAMain updateReadings() { NAMain result = null; - NAStationDataBody stationDataBody = getBridgeHandler().getStationsDataBody(getId()); + NetatmoBridgeHandler bridgeHandler = getBridgeHandler(); + NAStationDataBody stationDataBody = bridgeHandler == null ? null : bridgeHandler.getStationsDataBody(getId()); if (stationDataBody != null) { result = stationDataBody.getDevices().stream().filter(device -> device.getId().equalsIgnoreCase(getId())) .findFirst().orElse(null); @@ -64,12 +67,14 @@ protected NAMain updateReadings() { updateMeasurements(); - childs.keySet().forEach((childId) -> { - Optional childHandler = getBridgeHandler().findNAThing(childId); - childHandler.map(NetatmoModuleHandler.class::cast).ifPresent(naChildModule -> { - naChildModule.updateMeasurements(); + if (bridgeHandler != null) { + childs.keySet().forEach((childId) -> { + Optional childHandler = bridgeHandler.findNAThing(childId); + childHandler.map(NetatmoModuleHandler.class::cast).ifPresent(naChildModule -> { + naChildModule.updateMeasurements(); + }); }); - }); + } return result; } @@ -106,7 +111,7 @@ private void updateDayMeasurements() { addMeasurement(channels, types, CHANNEL_DATE_MIN_PRESSURE, DATE_MIN_PRESSURE); addMeasurement(channels, types, CHANNEL_DATE_MAX_PRESSURE, DATE_MAX_PRESSURE); if (!channels.isEmpty()) { - getMeasurements(getBridgeHandler(), getId(), null, ONE_DAY, types, channels, channelMeasurements); + getMeasurements(getId(), null, ONE_DAY, types, channels, channelMeasurements); } } @@ -134,7 +139,7 @@ private void updateWeekMeasurements() { addMeasurement(channels, types, CHANNEL_DATE_MIN_TEMP_THIS_WEEK, DATE_MIN_TEMP); addMeasurement(channels, types, CHANNEL_DATE_MAX_TEMP_THIS_WEEK, DATE_MAX_TEMP); if (!channels.isEmpty()) { - getMeasurements(getBridgeHandler(), getId(), null, ONE_WEEK, types, channels, channelMeasurements); + getMeasurements(getId(), null, ONE_WEEK, types, channels, channelMeasurements); } } @@ -162,14 +167,15 @@ private void updateMonthMeasurements() { addMeasurement(channels, types, CHANNEL_DATE_MIN_TEMP_THIS_MONTH, DATE_MIN_TEMP); addMeasurement(channels, types, CHANNEL_DATE_MAX_TEMP_THIS_MONTH, DATE_MAX_TEMP); if (!channels.isEmpty()) { - getMeasurements(getBridgeHandler(), getId(), null, ONE_MONTH, types, channels, channelMeasurements); + getMeasurements(getId(), null, ONE_MONTH, types, channels, channelMeasurements); } } @Override - protected State getNAThingProperty(@NonNull String channelId) { - if (device != null) { - NADashboardData dashboardData = device.getDashboardData(); + protected State getNAThingProperty(String channelId) { + NAMain mainDevice = device; + if (mainDevice != null) { + NADashboardData dashboardData = mainDevice.getDashboardData(); if (dashboardData != null) { switch (channelId) { case CHANNEL_CO2: @@ -288,8 +294,9 @@ protected State getNAThingProperty(@NonNull String channelId) { @Override protected @Nullable Integer getDataTimestamp() { - if (device != null) { - Integer lastStored = device.getLastStatusStore(); + NAMain mainDevice = device; + if (mainDevice != null) { + Integer lastStored = mainDevice.getLastStatusStore(); if (lastStored != null) { return lastStored; } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule1Handler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule1Handler.java index 42f534ac53ffe..80c814d521c1f 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule1Handler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule1Handler.java @@ -20,7 +20,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.types.State; @@ -38,6 +38,7 @@ * @author Rob Nielsen - Added day, week, and month measurements to the weather station and modules * */ +@NonNullByDefault public class NAModule1Handler extends NetatmoModuleHandler { private Map channelMeasurements = new ConcurrentHashMap<>(); @@ -65,7 +66,7 @@ private void updateDayMeasurements() { addMeasurement(channels, types, CHANNEL_DATE_MIN_HUMIDITY, DATE_MIN_HUM); addMeasurement(channels, types, CHANNEL_DATE_MAX_HUMIDITY, DATE_MAX_HUM); if (!channels.isEmpty()) { - getMeasurements(getBridgeHandler(), getParentId(), getId(), ONE_DAY, types, channels, channelMeasurements); + getMeasurements(getParentId(), getId(), ONE_DAY, types, channels, channelMeasurements); } } @@ -81,7 +82,7 @@ private void updateWeekMeasurements() { addMeasurement(channels, types, CHANNEL_DATE_MIN_TEMP_THIS_WEEK, DATE_MIN_TEMP); addMeasurement(channels, types, CHANNEL_DATE_MAX_TEMP_THIS_WEEK, DATE_MAX_TEMP); if (!channels.isEmpty()) { - getMeasurements(getBridgeHandler(), getParentId(), getId(), ONE_WEEK, types, channels, channelMeasurements); + getMeasurements(getParentId(), getId(), ONE_WEEK, types, channels, channelMeasurements); } } @@ -97,15 +98,15 @@ private void updateMonthMeasurements() { addMeasurement(channels, types, CHANNEL_DATE_MIN_TEMP_THIS_MONTH, DATE_MIN_TEMP); addMeasurement(channels, types, CHANNEL_DATE_MAX_TEMP_THIS_MONTH, DATE_MAX_TEMP); if (!channels.isEmpty()) { - getMeasurements(getBridgeHandler(), getParentId(), getId(), ONE_MONTH, types, channels, - channelMeasurements); + getMeasurements(getParentId(), getId(), ONE_MONTH, types, channels, channelMeasurements); } } @Override - protected State getNAThingProperty(@NonNull String channelId) { - if (module != null) { - NADashboardData dashboardData = module.getDashboardData(); + protected State getNAThingProperty(String channelId) { + NAStationModule stationModule = module; + if (stationModule != null) { + NADashboardData dashboardData = stationModule.getDashboardData(); if (dashboardData != null) { switch (channelId) { case CHANNEL_TEMP_TREND: diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule2Handler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule2Handler.java index 130ddda1ae640..459f36c566270 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule2Handler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule2Handler.java @@ -15,7 +15,7 @@ import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.*; import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*; -import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.types.State; @@ -30,6 +30,7 @@ * * @author Gaël L'hopital - Initial contribution */ +@NonNullByDefault public class NAModule2Handler extends NetatmoModuleHandler { public NAModule2Handler(Thing thing, final TimeZoneProvider timeZoneProvider) { @@ -42,9 +43,10 @@ protected void updateProperties(NAStationModule moduleData) { } @Override - protected State getNAThingProperty(@NonNull String channelId) { - if (module != null) { - NADashboardData dashboardData = module.getDashboardData(); + protected State getNAThingProperty(String channelId) { + NAStationModule stationModule = module; + if (stationModule != null) { + NADashboardData dashboardData = stationModule.getDashboardData(); if (dashboardData != null) { switch (channelId) { case CHANNEL_WIND_ANGLE: diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule3Handler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule3Handler.java index 614a2e4a27a12..556440bfd720c 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule3Handler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule3Handler.java @@ -20,7 +20,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.types.State; @@ -37,6 +37,7 @@ * @author Rob Nielsen - Added day, week, and month measurements to the weather station and modules * */ +@NonNullByDefault public class NAModule3Handler extends NetatmoModuleHandler { private Map channelMeasurements = new ConcurrentHashMap<>(); @@ -51,23 +52,24 @@ protected void updateProperties(NAStationModule moduleData) { @Override public void updateMeasurements() { - List<@NonNull String> types = Arrays.asList(SUM_RAIN); + List types = Arrays.asList(SUM_RAIN); if (isLinked(CHANNEL_SUM_RAIN_THIS_WEEK)) { - getMeasurements(getBridgeHandler(), getParentId(), getId(), ONE_WEEK, types, - Arrays.asList(CHANNEL_SUM_RAIN_THIS_WEEK), channelMeasurements); + getMeasurements(getParentId(), getId(), ONE_WEEK, types, Arrays.asList(CHANNEL_SUM_RAIN_THIS_WEEK), + channelMeasurements); } if (isLinked(CHANNEL_SUM_RAIN_THIS_MONTH)) { - getMeasurements(getBridgeHandler(), getParentId(), getId(), ONE_MONTH, types, - Arrays.asList(CHANNEL_SUM_RAIN_THIS_MONTH), channelMeasurements); + getMeasurements(getParentId(), getId(), ONE_MONTH, types, Arrays.asList(CHANNEL_SUM_RAIN_THIS_MONTH), + channelMeasurements); } } @Override - protected State getNAThingProperty(@NonNull String channelId) { - if (module != null) { - NADashboardData dashboardData = module.getDashboardData(); + protected State getNAThingProperty(String channelId) { + NAStationModule stationModule = module; + if (stationModule != null) { + NADashboardData dashboardData = stationModule.getDashboardData(); if (dashboardData != null) { switch (channelId) { case CHANNEL_RAIN: diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule4Handler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule4Handler.java index 1ab0402116173..a86f5a3fd663a 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule4Handler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/station/NAModule4Handler.java @@ -20,7 +20,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.types.State; @@ -38,6 +38,7 @@ * @author Rob Nielsen - Added day, week, and month measurements to the weather station and modules * */ +@NonNullByDefault public class NAModule4Handler extends NetatmoModuleHandler { private Map channelMeasurements = new ConcurrentHashMap<>(); @@ -69,7 +70,7 @@ private void updateDayMeasurements() { addMeasurement(channels, types, CHANNEL_DATE_MIN_HUMIDITY, DATE_MIN_HUM); addMeasurement(channels, types, CHANNEL_DATE_MAX_HUMIDITY, DATE_MAX_HUM); if (!channels.isEmpty()) { - getMeasurements(getBridgeHandler(), getParentId(), getId(), ONE_DAY, types, channels, channelMeasurements); + getMeasurements(getParentId(), getId(), ONE_DAY, types, channels, channelMeasurements); } } @@ -89,7 +90,7 @@ private void updateWeekMeasurements() { addMeasurement(channels, types, CHANNEL_DATE_MIN_TEMP_THIS_WEEK, DATE_MIN_TEMP); addMeasurement(channels, types, CHANNEL_DATE_MAX_TEMP_THIS_WEEK, DATE_MAX_TEMP); if (!channels.isEmpty()) { - getMeasurements(getBridgeHandler(), getParentId(), getId(), ONE_WEEK, types, channels, channelMeasurements); + getMeasurements(getParentId(), getId(), ONE_WEEK, types, channels, channelMeasurements); } } @@ -109,15 +110,15 @@ private void updateMonthMeasurements() { addMeasurement(channels, types, CHANNEL_DATE_MIN_TEMP_THIS_MONTH, DATE_MIN_TEMP); addMeasurement(channels, types, CHANNEL_DATE_MAX_TEMP_THIS_MONTH, DATE_MAX_TEMP); if (!channels.isEmpty()) { - getMeasurements(getBridgeHandler(), getParentId(), getId(), ONE_MONTH, types, channels, - channelMeasurements); + getMeasurements(getParentId(), getId(), ONE_MONTH, types, channels, channelMeasurements); } } @Override - protected State getNAThingProperty(@NonNull String channelId) { - if (module != null) { - NADashboardData dashboardData = module.getDashboardData(); + protected State getNAThingProperty(String channelId) { + NAStationModule stationModule = module; + if (stationModule != null) { + NADashboardData dashboardData = stationModule.getDashboardData(); if (dashboardData != null) { switch (channelId) { case CHANNEL_TEMP_TREND: diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/thermostat/NAPlugHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/thermostat/NAPlugHandler.java index e49e88c60da5e..0ab3a2e0d2cb4 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/thermostat/NAPlugHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/thermostat/NAPlugHandler.java @@ -18,12 +18,13 @@ import java.time.ZonedDateTime; import java.time.temporal.TemporalAdjusters; -import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.types.State; import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler; import org.openhab.binding.netatmo.internal.handler.NetatmoDeviceHandler; import io.swagger.client.model.NAPlug; @@ -37,16 +38,19 @@ * @author Gaël L'hopital - Initial contribution OH2 version * */ +@NonNullByDefault public class NAPlugHandler extends NetatmoDeviceHandler { - public NAPlugHandler(@NonNull Thing thing, final TimeZoneProvider timeZoneProvider) { + public NAPlugHandler(Thing thing, final TimeZoneProvider timeZoneProvider) { super(thing, timeZoneProvider); } @Override - protected NAPlug updateReadings() { + protected @Nullable NAPlug updateReadings() { NAPlug result = null; - NAThermostatDataBody thermostatDataBody = getBridgeHandler().getThermostatsDataBody(getId()); + NetatmoBridgeHandler bridgeHandler = getBridgeHandler(); + NAThermostatDataBody thermostatDataBody = bridgeHandler == null ? null + : bridgeHandler.getThermostatsDataBody(getId()); if (thermostatDataBody != null) { result = thermostatDataBody.getDevices().stream().filter(device -> device.getId().equalsIgnoreCase(getId())) .findFirst().orElse(null); @@ -63,12 +67,13 @@ protected void updateProperties(NAPlug deviceData) { } @Override - protected State getNAThingProperty(@NonNull String channelId) { + protected State getNAThingProperty(String channelId) { + NAPlug plugDevice = device; switch (channelId) { case CHANNEL_CONNECTED_BOILER: - return device != null ? toOnOffType(device.getPlugConnectedBoiler()) : UnDefType.UNDEF; + return plugDevice != null ? toOnOffType(plugDevice.getPlugConnectedBoiler()) : UnDefType.UNDEF; case CHANNEL_LAST_PLUG_SEEN: - return device != null ? toDateTimeType(device.getLastPlugSeen(), timeZoneProvider.getTimeZone()) + return plugDevice != null ? toDateTimeType(plugDevice.getLastPlugSeen(), timeZoneProvider.getTimeZone()) : UnDefType.UNDEF; case CHANNEL_LAST_BILAN: return toDateTimeType(getLastBilan()); @@ -77,8 +82,9 @@ protected State getNAThingProperty(@NonNull String channelId) { } public @Nullable ZonedDateTime getLastBilan() { - if (device != null) { - NAYearMonth lastBilan = device.getLastBilan(); + NAPlug plugDevice = device; + if (plugDevice != null) { + NAYearMonth lastBilan = plugDevice.getLastBilan(); if (lastBilan != null) { ZonedDateTime zonedDT = ZonedDateTime.of(lastBilan.getY(), lastBilan.getM(), 1, 0, 0, 0, 0, ZonedDateTime.now().getZone()); @@ -90,8 +96,9 @@ protected State getNAThingProperty(@NonNull String channelId) { @Override protected @Nullable Integer getDataTimestamp() { - if (device != null) { - Integer lastStored = device.getLastStatusStore(); + NAPlug plugDevice = device; + if (plugDevice != null) { + Integer lastStored = plugDevice.getLastStatusStore(); if (lastStored != null) { return lastStored; } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/thermostat/NATherm1Handler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/thermostat/NATherm1Handler.java index bc14a079d1ea1..843af819ec97a 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/thermostat/NATherm1Handler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/thermostat/NATherm1Handler.java @@ -25,7 +25,7 @@ import javax.measure.quantity.Temperature; -import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.library.types.OnOffType; @@ -61,11 +61,12 @@ * @author Gaël L'hopital - Initial contribution OH2 version * */ +@NonNullByDefault public class NATherm1Handler extends NetatmoModuleHandler { private final Logger logger = LoggerFactory.getLogger(NATherm1Handler.class); private final NATherm1StateDescriptionProvider stateDescriptionProvider; - public NATherm1Handler(@NonNull Thing thing, NATherm1StateDescriptionProvider stateDescriptionProvider, + public NATherm1Handler(Thing thing, NATherm1StateDescriptionProvider stateDescriptionProvider, final TimeZoneProvider timeZoneProvider) { super(thing, timeZoneProvider); this.stateDescriptionProvider = stateDescriptionProvider; @@ -82,7 +83,7 @@ protected void updateProperties(NAThermostat moduleData) { } @Override - public void updateChannels(Object moduleObject) { + public void updateChannels(@Nullable Object moduleObject) { if (isRefreshRequired()) { measurableChannels.getAsCsv().ifPresent(csvParams -> { ThermostatApi thermostatApi = getThermostatApi(); @@ -96,8 +97,9 @@ public void updateChannels(Object moduleObject) { } super.updateChannels(moduleObject); - if (module != null) { - updateStateDescription(module); + NAThermostat thermostat = module; + if (thermostat != null) { + updateStateDescription(thermostat); } } @@ -109,30 +111,32 @@ private void updateStateDescription(NAThermostat thermostat) { stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_PLANNING), options); } - @SuppressWarnings("null") @Override - protected State getNAThingProperty(@NonNull String channelId) { + protected State getNAThingProperty(String channelId) { + NAThermostat thermostat = module; switch (channelId) { case CHANNEL_THERM_ORIENTATION: - return module != null ? toDecimalType(module.getThermOrientation()) : UnDefType.UNDEF; + return thermostat != null ? toDecimalType(thermostat.getThermOrientation()) : UnDefType.UNDEF; case CHANNEL_THERM_RELAY: - return module != null ? module.getThermRelayCmd() == 100 ? OnOffType.ON : OnOffType.OFF + return thermostat != null ? thermostat.getThermRelayCmd() == 100 ? OnOffType.ON : OnOffType.OFF : UnDefType.UNDEF; case CHANNEL_TEMPERATURE: - return module != null ? toQuantityType(module.getMeasured().getTemperature(), API_TEMPERATURE_UNIT) + return thermostat != null + ? toQuantityType(thermostat.getMeasured().getTemperature(), API_TEMPERATURE_UNIT) : UnDefType.UNDEF; case CHANNEL_SETPOINT_TEMP: return getCurrentSetpoint(); case CHANNEL_TIMEUTC: - return module != null ? toDateTimeType(module.getMeasured().getTime(), timeZoneProvider.getTimeZone()) + return thermostat != null + ? toDateTimeType(thermostat.getMeasured().getTime(), timeZoneProvider.getTimeZone()) : UnDefType.UNDEF; case CHANNEL_SETPOINT_END_TIME: { - if (module != null) { - NASetpoint setpoint = module.getSetpoint(); + if (thermostat != null) { + NASetpoint setpoint = thermostat.getSetpoint(); if (setpoint != null) { Integer endTime = setpoint.getSetpointEndtime(); if (endTime == null) { - endTime = getNextProgramTime(module.getThermProgramList()); + endTime = getNextProgramTime(thermostat.getThermProgramList()); } return toDateTimeType(endTime, timeZoneProvider.getTimeZone()); } @@ -144,8 +148,8 @@ protected State getNAThingProperty(@NonNull String channelId) { return getSetpoint(); case CHANNEL_PLANNING: { String currentPlanning = "-"; - if (module != null) { - for (NAThermProgram program : module.getThermProgramList()) { + if (thermostat != null) { + for (NAThermProgram program : thermostat.getThermProgramList()) { if (program.getSelected() == Boolean.TRUE) { currentPlanning = program.getProgramId(); } @@ -157,20 +161,21 @@ protected State getNAThingProperty(@NonNull String channelId) { return super.getNAThingProperty(channelId); } - @SuppressWarnings("null") private State getSetpoint() { - return module != null - ? module.getSetpoint() != null ? toStringType(module.getSetpoint().getSetpointMode()) : UnDefType.NULL + NAThermostat thermostat = module; + return thermostat != null + ? thermostat.getSetpoint() != null ? toStringType(thermostat.getSetpoint().getSetpointMode()) + : UnDefType.NULL : UnDefType.UNDEF; } - @SuppressWarnings("null") private State getCurrentSetpoint() { - if (module != null && module.getSetpoint() != null) { - NASetpoint setPoint = module.getSetpoint(); + NAThermostat thermostat = module; + if (thermostat != null && thermostat.getSetpoint() != null) { + NASetpoint setPoint = thermostat.getSetpoint(); String currentMode = setPoint.getSetpointMode(); - NAThermProgram currentProgram = module.getThermProgramList().stream() + NAThermProgram currentProgram = thermostat.getThermProgramList().stream() .filter(p -> p.getSelected() != null && p.getSelected()).findFirst().get(); switch (currentMode) { case CHANNEL_SETPOINT_MODE_MANUAL: @@ -182,7 +187,7 @@ private State getCurrentSetpoint() { NAZone zone1 = getZone(currentProgram.getZones(), 3); return toDecimalType(zone1.getTemp()); case CHANNEL_SETPOINT_MODE_PROGRAM: - NATimeTableItem currentProgramMode = getCurrentProgramMode(module.getThermProgramList()); + NATimeTableItem currentProgramMode = getCurrentProgramMode(thermostat.getThermProgramList()); if (currentProgramMode != null) { NAZone zone2 = getZone(currentProgram.getZones(), currentProgramMode.getId()); return toDecimalType(zone2.getTemp()); @@ -208,7 +213,7 @@ private long getNetatmoProgramBaseTime() { return mondayZero.getTimeInMillis(); } - private NATimeTableItem getCurrentProgramMode(List thermProgramList) { + private @Nullable NATimeTableItem getCurrentProgramMode(List thermProgramList) { NATimeTableItem lastProgram = null; Calendar now = Calendar.getInstance(); long diff = (now.getTimeInMillis() - getNetatmoProgramBaseTime()) / 1000 / 60; @@ -219,7 +224,10 @@ private NATimeTableItem getCurrentProgramMode(List thermProgramL if (currentProgram.isPresent()) { Stream pastPrograms = currentProgram.get().getTimetable().stream() .filter(t -> t.getMOffset() < diff); - lastProgram = pastPrograms.reduce((first, second) -> second).orElse(null); + Optional program = pastPrograms.reduce((first, second) -> second); + if (program.isPresent()) { + lastProgram = program.get(); + } } return lastProgram; @@ -300,7 +308,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - private void pushSetpointUpdate(String target_mode, Integer setpointEndtime, Float setpointTemp) { + private void pushSetpointUpdate(String target_mode, @Nullable Integer setpointEndtime, + @Nullable Float setpointTemp) { ThermostatApi thermostatApi = getThermostatApi(); if (thermostatApi != null) { thermostatApi.setthermpoint(getParentId(), getId(), target_mode, setpointEndtime, setpointTemp); diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/webhook/WelcomeWebHookServlet.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/webhook/WelcomeWebHookServlet.java index 2364ae3e22d57..8929af01255db 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/webhook/WelcomeWebHookServlet.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/webhook/WelcomeWebHookServlet.java @@ -20,6 +20,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler; import org.osgi.service.http.HttpService; import org.osgi.service.http.NamespaceException; @@ -33,17 +35,19 @@ * * @author Gaël L'hopital - Initial contribution */ +@NonNullByDefault public class WelcomeWebHookServlet extends HttpServlet { private static final long serialVersionUID = 1288539782077957954L; private static final String PATH = "/netatmo/%s/camera"; private static final String APPLICATION_JSON = "application/json"; private static final String CHARSET = "utf-8"; + private final Gson gson = new Gson(); private final Logger logger = LoggerFactory.getLogger(WelcomeWebHookServlet.class); private HttpService httpService; - private NetatmoBridgeHandler bridgeHandler; + private @Nullable NetatmoBridgeHandler bridgeHandler; private String path; public WelcomeWebHookServlet(HttpService httpService, String id) { @@ -76,12 +80,18 @@ public void deactivate() { } @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void service(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) + throws ServletException, IOException { + if (req == null || resp == null) { + return; + } + String data = inputStreamToString(req); - if (data != null && bridgeHandler != null) { + NetatmoBridgeHandler handler = bridgeHandler; + if (!data.isEmpty() && handler != null) { NAWebhookCameraEvent event = gson.fromJson(data, NAWebhookCameraEvent.class); logger.debug("Event transmitted from restService"); - bridgeHandler.webHookEvent(event); + handler.webHookEvent(event); } setHeaders(resp); From e60d243acc17c1f8061276e60f66eed4896b7f3a Mon Sep 17 00:00:00 2001 From: lolodomo Date: Sun, 28 Jun 2020 10:21:19 +0200 Subject: [PATCH 12/85] [ntp] Updated documentation (#8010) i18n entries that are no more used are removed Signed-off-by: Laurent Garnier --- bundles/org.openhab.binding.ntp/README.md | 10 +++++----- .../src/main/resources/ESH-INF/i18n/ntp.properties | 1 - .../src/main/resources/ESH-INF/i18n/ntp_de.properties | 3 --- .../src/main/resources/ESH-INF/i18n/ntp_fr.properties | 3 --- .../src/main/resources/ESH-INF/i18n/ntp_tr.properties | 3 --- 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/bundles/org.openhab.binding.ntp/README.md b/bundles/org.openhab.binding.ntp/README.md index 926dce9de8701..1e0dc9c846e28 100644 --- a/bundles/org.openhab.binding.ntp/README.md +++ b/bundles/org.openhab.binding.ntp/README.md @@ -20,11 +20,11 @@ The thing has a few configuration options: | Option | Description | |-----------------|--------------------------------------------------- | -| hostname | NTP host server, e.g. nl.pool.ntp.org | -| refreshInterval | Interval that new time updates are posted to the eventbus in seconds | -| refreshNtp | Number of updates between querying the NTP server (e.g. with refreshinterval = 60 (seconds) and refreshNtp = 30 the NTP server is queried each half hour. | -| timeZone | Timezone, can be left blank for using the default system one | -| locale | Locale, can be left blank for using the default system one | +| hostname | The NTP server hostname, e.g. nl.pool.ntp.org | +| refreshInterval | Interval that new time updates are posted to the eventbus in seconds. Default is 60s. | +| refreshNtp | Number of updates between querying the NTP server (e.g. with refreshinterval = 60 (seconds) and refreshNtp = 30 the NTP server is queried each half hour). Default is 30. | +| serverPort | The port that the NTP server could use. Default is 123. | +| timeZone | The configured timezone. Can be left blank for using the timezone defined as openHAB configuration setting (or default system timezone if not defined). | ## Channels diff --git a/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp.properties b/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp.properties index fb2b27dddca94..8cd4b30e61db1 100644 --- a/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp.properties +++ b/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp.properties @@ -1,5 +1,4 @@ # Thing status descriptions -offline.conf-error-init-handler = An error occurred while initializing the NTP handler. offline.comm-error-unknown-host = The timeserver hostname {0} is unknown -> returning current sytem time instead. offline.comm-error-connection = The network connection to the timeserver {0} cannot be established -> returning current sytem time instead. diff --git a/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp_de.properties b/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp_de.properties index 1e4fd8a67d38c..c59955c87def1 100644 --- a/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp_de.properties +++ b/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp_de.properties @@ -17,8 +17,6 @@ thing-type.config.ntp.ntp.serverPort.label = Port thing-type.config.ntp.ntp.serverPort.description = Port des Zeitservers. thing-type.config.ntp.ntp.timeZone.label = Zeitzone thing-type.config.ntp.ntp.timeZone.description = Zeitzone des Systems. -thing-type.config.ntp.ntp.locale.label = Locale -thing-type.config.ntp.ntp.locale.description = Gebietseinstellung des Systems. # channel types channel-type.ntp.dateTime-channel.label = Datum und Zeit @@ -31,7 +29,6 @@ channel-type.config.ntp.string-channel.DateTimeFormat.label = Datumsformat channel-type.config.ntp.string-channel.DateTimeFormat.description = Format fr die Anzeige des Datum und der Zeit. # Thing status descriptions -offline.conf-error-init-handler = Fehler bei der Initialisierung des NTP handler. offline.comm-error-unknown-host = Zeitserver {0} ist unbekannt. Systemzeit wird zurckgegeben. offline.comm-error-connection = Verbindung zum Zeitserver {0} kann nicht aufgebaut werden. Systemzeit wird zurckgegeben. diff --git a/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp_fr.properties b/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp_fr.properties index 02b8af065466f..b6bfb2904b0d4 100644 --- a/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp_fr.properties +++ b/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp_fr.properties @@ -17,8 +17,6 @@ thing-type.config.ntp.ntp.serverPort.label = Port du serveur thing-type.config.ntp.ntp.serverPort.description = Le port que le serveur de temps peut utiliser. thing-type.config.ntp.ntp.timeZone.label = Fuseau horaire thing-type.config.ntp.ntp.timeZone.description = Le fuseau horaire slectionn. -thing-type.config.ntp.ntp.locale.label = Paramtres rgionaux -thing-type.config.ntp.ntp.locale.description = La langue et le pays slectionns. # channel types channel-type.ntp.dateTime-channel.label = Date heure @@ -31,7 +29,6 @@ channel-type.config.ntp.string-channel.DateTimeFormat.label = Format date et heu channel-type.config.ntp.string-channel.DateTimeFormat.description = Format utilis pour la prsentation de la date et de l'heure. # Thing status descriptions -offline.conf-error-init-handler = Une erreur est survenue durant l''initialisation. offline.comm-error-unknown-host = Le nom d''hte {0} du serveur de temps est inconnu -> renvoi de l''heure courante systme la place. offline.comm-error-connection = La connexion rseau avec le serveur de temps {0} ne peut pas tre tablie -> renvoi de l''heure courante systme la place. diff --git a/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp_tr.properties b/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp_tr.properties index f8ac45e71f497..71db833005b97 100644 --- a/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp_tr.properties +++ b/bundles/org.openhab.binding.ntp/src/main/resources/ESH-INF/i18n/ntp_tr.properties @@ -17,8 +17,6 @@ thing-type.config.ntp.ntp.serverPort.label = Ba\u011flant\u0131 noktas\u0131 thing-type.config.ntp.ntp.serverPort.description = Zaman sunucunun ba\u011flant\u0131 noktas\u0131 thing-type.config.ntp.ntp.timeZone.label = Saat dilimi thing-type.config.ntp.ntp.timeZone.description = Sistemin saat dilimi -thing-type.config.ntp.ntp.locale.label = Yerel ayar -thing-type.config.ntp.ntp.locale.description = Sistemin yerel ayar\u0131 # channel types channel-type.ntp.dateTime-channel.label = Tarih ve saat @@ -31,7 +29,6 @@ channel-type.config.ntp.string-channel.DateTimeFormat.label = Tarih bi channel-type.config.ntp.string-channel.DateTimeFormat.description = Tarih ve saat grnteleme biimi. # Thing status descriptions -offline.conf-error-init-handler = NTP ba\u011flant\u0131s\u0131n\u0131 ba\u015flat\u0131rken hata olu\u015ftu. offline.comm-error-unknown-host = Zaman sunucusu {0} tan\u0131nm\u0131yor. Sistem tarihi ve saati sunulacakt\u0131r. offline.comm-error-connection = {0} zaman sunucusuna ba\u011flan\u0131lam\u0131yor. Sistem zaman\u0131 sunulacakt\u0131r. From 652b610f56f6c02a1bff2d1054fdbbc4cbc286d9 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Sun, 28 Jun 2020 04:27:52 -0600 Subject: [PATCH 13/85] [homekit] change SECURED=OFF for HomeKit lock (#7984) * [homekit] allow exposing jammed state for a lock also change SECURED to be ON for SwitchItem since this is what most real locks (especially ZWave ones) do Signed-off-by: Cody Cutrer --- bundles/org.openhab.io.homekit/README.md | 4 ++-- .../internal/accessories/HomekitLockImpl.java | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/bundles/org.openhab.io.homekit/README.md b/bundles/org.openhab.io.homekit/README.md index 1e41454e08601..552cd6adf0507 100644 --- a/bundles/org.openhab.io.homekit/README.md +++ b/bundles/org.openhab.io.homekit/README.md @@ -248,8 +248,8 @@ A full list of supported accessory types can be found in the table *below*. | | | CoolingThresholdTemperature | Number | maximum temperature that must be reached before cooling is turned on. min/max/step can configured at item level, e.g. minValue=10.5, maxValue=50, step=2] | | | | HeatingThresholdTemperature | Number | minimum temperature that must be reached before heating is turned on. min/max/step can configured at item level, e.g. minValue=10.5, maxValue=50, step=2] | | Lock | | | | A Lock Mechanism | -| | LockCurrentState | | Switch | current states of lock mechanism (OFF=SECURED, ON=UNSECURED) | -| | LockTargetState | | Switch | target states of lock mechanism (OFF=SECURED, ON=UNSECURED) | +| | LockCurrentState | | Switch,Number | current state of lock mechanism (1/ON=SECURED, 0/OFF=UNSECURED, 2=JAMMED, 3=UNKNOWN) | +| | LockTargetState | | Switch | target state of lock mechanism (ON=SECURED, OFF=UNSECURED) | | | | Name | String | Name of the lock | | Valve | | | | Valve. additional configuration: homekitValveType = ["Generic", "Irrigation", "Shower", "Faucet"] | | | ActiveStatus | | Switch | accessory current working status. A value of "ON"/"OPEN" indicate that the accessory is active and is functioning without any errors. | diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLockImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLockImpl.java index b0c8b9fbad8f5..b4645c84989d1 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLockImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLockImpl.java @@ -16,9 +16,12 @@ import java.util.concurrent.CompletableFuture; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.items.Item; import org.eclipse.smarthome.core.library.items.SwitchItem; +import org.eclipse.smarthome.core.library.types.DecimalType; import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.types.State; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; import org.openhab.io.homekit.internal.HomekitCharacteristicType; import org.openhab.io.homekit.internal.HomekitSettings; @@ -47,10 +50,14 @@ public HomekitLockImpl(HomekitTaggedItem taggedItem, List man @Override public CompletableFuture getLockCurrentState() { @Nullable - OnOffType state = getStateAs(HomekitCharacteristicType.LOCK_CURRENT_STATE, OnOffType.class); - if (state != null) { + + final State state = getItem(HomekitCharacteristicType.LOCK_CURRENT_STATE, GenericItem.class).getState(); + if (state instanceof DecimalType) { + return CompletableFuture.completedFuture(LockCurrentStateEnum.fromCode( + state.as(DecimalType.class).intValue())); + } else if (state instanceof OnOffType) { return CompletableFuture.completedFuture( - state == OnOffType.OFF ? LockCurrentStateEnum.SECURED : LockCurrentStateEnum.UNSECURED); + state == OnOffType.ON ? LockCurrentStateEnum.SECURED : LockCurrentStateEnum.UNSECURED); } return CompletableFuture.completedFuture(LockCurrentStateEnum.UNKNOWN); } @@ -61,7 +68,7 @@ public CompletableFuture getLockTargetState() { OnOffType state = getStateAs(HomekitCharacteristicType.LOCK_TARGET_STATE, OnOffType.class); if (state != null) { return CompletableFuture.completedFuture( - state == OnOffType.OFF ? LockTargetStateEnum.SECURED : LockTargetStateEnum.UNSECURED); + state == OnOffType.ON ? LockTargetStateEnum.SECURED : LockTargetStateEnum.UNSECURED); } return CompletableFuture.completedFuture(LockTargetStateEnum.UNSECURED); // Apple HAP specification has onyl SECURED and UNSECURED values for lock target state. @@ -77,13 +84,13 @@ public CompletableFuture setLockTargetState(final LockTargetStateEnum stat case SECURED: // Close the door if (item instanceof SwitchItem) { - ((SwitchItem) item).send(OnOffType.OFF); + ((SwitchItem) item).send(OnOffType.ON); } break; case UNSECURED: // Open the door if (item instanceof SwitchItem) { - ((SwitchItem) item).send(OnOffType.ON); + ((SwitchItem) item).send(OnOffType.OFF); } break; default: From db07d61f7ae8961a33973ab439dbeaaf1004ccbd Mon Sep 17 00:00:00 2001 From: Chris Johnson Date: Sun, 28 Jun 2020 12:38:53 -0700 Subject: [PATCH 14/85] [somfymylink] add missing feature.xml and pom entry (#8024) Signed-off-by: Chris Johnson --- bom/openhab-addons/pom.xml | 5 +++++ .../src/feature/feature.xml | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 bundles/org.openhab.binding.somfymylink/src/feature/feature.xml diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index fa68d029a0823..e7923e55d499d 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -949,6 +949,11 @@ org.openhab.binding.somfytahoma ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.somfymylink + ${project.version} + org.openhab.addons.bundles org.openhab.binding.sonos diff --git a/bundles/org.openhab.binding.somfymylink/src/feature/feature.xml b/bundles/org.openhab.binding.somfymylink/src/feature/feature.xml new file mode 100644 index 0000000000000..6e8e7c3d2c3d1 --- /dev/null +++ b/bundles/org.openhab.binding.somfymylink/src/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.somfymylink/${project.version} + + From ac220fcb296f6d071ba921ad52c7150129de8ed7 Mon Sep 17 00:00:00 2001 From: Matt Ward Date: Sun, 28 Jun 2020 20:40:35 +0100 Subject: [PATCH 15/85] [tplinksmarthome] Added support for KP105 (#8023) Signed-off-by: Matt Ward --- .../README.md | 10 +++++++-- .../internal/TPLinkSmartHomeThingType.java | 1 + .../main/resources/ESH-INF/thing/KP105.xml | 22 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 bundles/org.openhab.binding.tplinksmarthome/src/main/resources/ESH-INF/thing/KP105.xml diff --git a/bundles/org.openhab.binding.tplinksmarthome/README.md b/bundles/org.openhab.binding.tplinksmarthome/README.md index bdec100852404..dffe861baf8ec 100644 --- a/bundles/org.openhab.binding.tplinksmarthome/README.md +++ b/bundles/org.openhab.binding.tplinksmarthome/README.md @@ -99,6 +99,12 @@ Switching, Brightness and Color is done using the `color` channel. * LED On/Off * Wi-Fi signal strength (RSSI) +### KP105 Kasa Wi-Fi Smart Plug - Slim Edition + +* Power On/Off +* LED On/Off +* Wi-Fi signal strength (RSSI) + ### KP200 Smart Wi-Fi Power Outlet, 2-Sockets * Power On/Off Group @@ -285,7 +291,7 @@ All devices support some of the following channels: | Channel Type ID | Item Type | Description | Thing types supporting this channel | |---------------------|--------------------------|------------------------------------------------|----------------------------------------------------------------------------------------------------| -| switch | Switch | Power the device on or off. | HS100, HS103, HS105, HS107, HS110, HS200, HS210, HS300, KP100, KP200, KP303, KP400, RE270K, RE370K | +| switch | Switch | Power the device on or off. | HS100, HS103, HS105, HS107, HS110, HS200, HS210, HS300, KP100, KP105, KP200, KP303, KP400, RE270K, RE370K | | brightness | Dimmer | Set the brightness of device or dimmer. | HS220, KB100, KL50, KL60, KL110, KL120, LB100, LB110, LB120, LB200 | | colorTemperature | Dimmer | Set the color temperature in percentage. | KB130, KL120, KL130, LB120, LB130, LB230 | | colorTemperatureAbs | Number | Set the color temperature in Kelvin. | KB130, KL120, KL130, LB120, LB130, LB230 | @@ -294,7 +300,7 @@ All devices support some of the following channels: | eneryUsage | Number:Energy | Energy Usage in kWh. | HS110, HS300 | | current | Number:ElectricCurrent | Actual current usage in Ampere. | HS110, HS300 | | voltage | Number:ElectricPotential | Actual voltage usage in Volt. | HS110, HS300 | -| led | Switch | Switch the status LED on the device on or off. | HS100, HS103, HS105, HS107, HS110, HS200, HS210, HS220, HS300, KP100, KP303, KP200, KP400 | +| led | Switch | Switch the status LED on the device on or off. | HS100, HS103, HS105, HS107, HS110, HS200, HS210, HS220, HS300, KP100, KP105, KP303, KP200, KP400 | | rssi | Number:Power | Wi-Fi signal strength indicator in dBm. | All | The outlet devices (HS107, HS300, KP200, KP400) have group channels. diff --git a/bundles/org.openhab.binding.tplinksmarthome/src/main/java/org/openhab/binding/tplinksmarthome/internal/TPLinkSmartHomeThingType.java b/bundles/org.openhab.binding.tplinksmarthome/src/main/java/org/openhab/binding/tplinksmarthome/internal/TPLinkSmartHomeThingType.java index 60ad062192a6f..a31c7a93133e1 100644 --- a/bundles/org.openhab.binding.tplinksmarthome/src/main/java/org/openhab/binding/tplinksmarthome/internal/TPLinkSmartHomeThingType.java +++ b/bundles/org.openhab.binding.tplinksmarthome/src/main/java/org/openhab/binding/tplinksmarthome/internal/TPLinkSmartHomeThingType.java @@ -53,6 +53,7 @@ public enum TPLinkSmartHomeThingType { HS105("hs105", DeviceType.PLUG), HS110("hs110", DeviceType.PLUG), KP100("kp100", DeviceType.PLUG), + KP105("kp105", DeviceType.PLUG), // Switch Thing Type UIDs HS200("hs200", DeviceType.SWITCH), diff --git a/bundles/org.openhab.binding.tplinksmarthome/src/main/resources/ESH-INF/thing/KP105.xml b/bundles/org.openhab.binding.tplinksmarthome/src/main/resources/ESH-INF/thing/KP105.xml new file mode 100644 index 0000000000000..29a516366f1b4 --- /dev/null +++ b/bundles/org.openhab.binding.tplinksmarthome/src/main/resources/ESH-INF/thing/KP105.xml @@ -0,0 +1,22 @@ + + + + + + TP-Link KP105 Kasa Wi-Fi Smart Plug - Slim Edition + PowerOutlet + + + + + + + + deviceId + + + + From 05eb61d95b0fddd49ab5f51b98c989b398a463e1 Mon Sep 17 00:00:00 2001 From: Sven Strohschein Date: Sun, 28 Jun 2020 22:29:13 +0200 Subject: [PATCH 16/85] [netatmo] Support for switching video surveillance on/off (#7968) * [7938] Support for switching video surveillance on/off - The video surveillance can now get switched on/off - When the command is executed (floodlight or video surveillance switched on/off) a refresh is executed after 2 seconds to update all channels * Warnings fixed * New tests for isNewLastEvent added Signed-off-by: Sven Strohschein --- bundles/org.openhab.binding.netatmo/README.md | 11 +- .../{presence => camera}/CameraAddress.java | 2 +- .../internal/camera/CameraHandler.java | 148 +++++++++++++----- .../handler/NetatmoModuleHandler.java | 6 +- .../presence/NAPresenceCameraHandler.java | 61 +------- .../welcome/NAWelcomeHomeHandler.java | 6 +- .../main/resources/ESH-INF/thing/camera.xml | 1 - .../presence/NAPresenceCameraHandlerTest.java | 65 ++++++-- .../welcome/NAWelcomeHomeHandlerTest.java | 127 ++++++++++++--- 9 files changed, 290 insertions(+), 137 deletions(-) rename bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/{presence => camera}/CameraAddress.java (96%) diff --git a/bundles/org.openhab.binding.netatmo/README.md b/bundles/org.openhab.binding.netatmo/README.md index 3c9bff21af24c..0bc2b43e47bc0 100644 --- a/bundles/org.openhab.binding.netatmo/README.md +++ b/bundles/org.openhab.binding.netatmo/README.md @@ -502,14 +502,17 @@ All these channels are read only. ### Welcome and Presence Camera -Warning: The URL of the live snapshot is a fixed URL so the value of the channel cameraLivePictureUrl / welcomeCameraLivePictureUrl will never be updated once first set by the binding. +Warnings: +- The URL of the live snapshot is a fixed URL so the value of the channel cameraLivePictureUrl / welcomeCameraLivePictureUrl will never be updated once first set by the binding. So to get a refreshed picture, you need to use the refresh parameter in your sitemap image element. +- Some features like the video surveillance are accessed via the local network, so it may be helpful to set a static IP address +for the camera within your local network. **Supported channels for the Welcome Camera thing:** | Channel ID | Item Type | Read/Write | Description | |-----------------------------|-----------|------------|--------------------------------------------------------------| -| welcomeCameraStatus | Switch | Read-only | State of the camera | +| welcomeCameraStatus | Switch | Read-write | State of the camera (video surveillance on/off) | | welcomeCameraSdStatus | Switch | Read-only | State of the SD card | | welcomeCameraAlimStatus | Switch | Read-only | State of the power connector | | welcomeCameraIsLocal | Switch | Read-only | indicates whether the camera is on the same network than the openHAB Netatmo Binding | @@ -520,15 +523,13 @@ So to get a refreshed picture, you need to use the refresh parameter in your sit **Supported channels for the Presence Camera thing:** Warnings: -- Some features like the floodlight are accessed via the local network, so it may be helpful to set a static IP address -for the Presence camera within your local network. - The floodlight auto-mode (cameraFloodlightAutoMode) isn't updated it is changed by another application. Therefore the binding handles its own state of the auto-mode. This has the advantage that the user can define its own floodlight switch off behaviour. | Channel ID | Item Type | Read/Write | Description | |-----------------------------|-----------|------------|--------------------------------------------------------------| -| cameraStatus | Switch | Read-only | State of the camera | +| cameraStatus | Switch | Read-write | State of the camera (video surveillance on/off) | | cameraSdStatus | Switch | Read-only | State of the SD card | | cameraAlimStatus | Switch | Read-only | State of the power connector | | cameraIsLocal | Switch | Read-only | indicates whether the camera is on the same network than the openHAB Netatmo Binding | diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/CameraAddress.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraAddress.java similarity index 96% rename from bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/CameraAddress.java rename to bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraAddress.java index ce55a1a5c3045..af54e36fa5e7a 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/CameraAddress.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraAddress.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.netatmo.internal.presence; +package org.openhab.binding.netatmo.internal.camera; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraHandler.java index 2c84de4eff119..7d152ee466dde 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraHandler.java @@ -16,15 +16,26 @@ import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.core.i18n.TimeZoneProvider; -import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.State; import org.eclipse.smarthome.core.types.UnDefType; import org.eclipse.smarthome.io.net.http.HttpUtil; +import org.json.JSONException; +import org.json.JSONObject; +import org.openhab.binding.netatmo.internal.ChannelTypeUtils; import org.openhab.binding.netatmo.internal.handler.NetatmoModuleHandler; import io.swagger.client.model.NAWelcomeCamera; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Optional; /** * {@link CameraHandler} is the class used to handle Camera Data @@ -33,20 +44,42 @@ * NAWelcomeCameraHandler) * */ +@NonNullByDefault public abstract class CameraHandler extends NetatmoModuleHandler { + private static final String PING_URL_PATH = "/command/ping"; + private static final String STATUS_CHANGE_URL_PATH = "/command/changestatus"; private static final String LIVE_PICTURE = "/live/snapshot_720.jpg"; - protected CameraHandler(@NonNull Thing thing, final TimeZoneProvider timeZoneProvider) { + private final Logger logger = LoggerFactory.getLogger(CameraHandler.class); + + private Optional cameraAddress = Optional.empty(); + + protected CameraHandler(Thing thing, final TimeZoneProvider timeZoneProvider) { super(thing, timeZoneProvider); } + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + String channelId = channelUID.getId(); + switch (channelId) { + case CHANNEL_CAMERA_STATUS: + case CHANNEL_WELCOME_CAMERA_STATUS: + if(command == OnOffType.ON) { + switchVideoSurveillance(true); + } else if(command == OnOffType.OFF) { + switchVideoSurveillance(false); + } + break; + } + super.handleCommand(channelUID, command); + } + @Override protected void updateProperties(NAWelcomeCamera moduleData) { updateProperties(null, moduleData.getType()); } - @SuppressWarnings("null") @Override protected State getNAThingProperty(@NonNull String channelId) { switch (channelId) { @@ -69,34 +102,32 @@ protected State getNAThingProperty(@NonNull String channelId) { } protected State getStatusState() { - return module != null ? toOnOffType(module.getStatus()) : UnDefType.UNDEF; + return getModule().map(m -> toOnOffType(m.getStatus())).orElse(UnDefType.UNDEF); } protected State getSdStatusState() { - return module != null ? toOnOffType(module.getSdStatus()) : UnDefType.UNDEF; + return getModule().map(m -> toOnOffType(m.getSdStatus())).orElse(UnDefType.UNDEF); } protected State getAlimStatusState() { - return module != null ? toOnOffType(module.getAlimStatus()) : UnDefType.UNDEF; + return getModule().map(m -> toOnOffType(m.getAlimStatus())).orElse(UnDefType.UNDEF); } protected State getIsLocalState() { - return module != null ? toOnOffType(module.getIsLocal()) : UnDefType.UNDEF; + return getModule().map(m -> toOnOffType(m.getIsLocal())).orElse(UnDefType.UNDEF); } protected State getLivePictureURLState() { - String livePictureURL = getLivePictureURL(); - return livePictureURL == null ? UnDefType.UNDEF : toStringType(livePictureURL); + return getLivePictureURL().map(ChannelTypeUtils::toStringType).orElse(UnDefType.UNDEF); } protected State getLivePictureState() { - String livePictureURL = getLivePictureURL(); - return livePictureURL == null ? UnDefType.UNDEF : HttpUtil.downloadImage(livePictureURL); + Optional livePictureURL = getLivePictureURL(); + return livePictureURL.isPresent() ? HttpUtil.downloadImage(livePictureURL.get()) : UnDefType.UNDEF; } protected State getLiveStreamState() { - String liveStreamURL = getLiveStreamURL(); - return liveStreamURL == null ? UnDefType.UNDEF : new StringType(liveStreamURL); + return getLiveStreamURL().map(ChannelTypeUtils::toStringType).orElse(UnDefType.UNDEF); } /** @@ -104,12 +135,8 @@ protected State getLiveStreamState() { * * @return Url of the live snapshot */ - private String getLivePictureURL() { - String result = getVpnUrl(); - if (result != null) { - result += LIVE_PICTURE; - } - return result; + private Optional getLivePictureURL() { + return getVpnUrl().map(u -> u += LIVE_PICTURE); } /** @@ -117,33 +144,32 @@ private String getLivePictureURL() { * * @return Url of the live stream */ - private String getLiveStreamURL() { - String result = getVpnUrl(); - if (result == null) { - return null; + private Optional getLiveStreamURL() { + Optional result = getVpnUrl(); + if (!result.isPresent()) { + return Optional.empty(); } - StringBuilder resultStringBuilder = new StringBuilder(result); + StringBuilder resultStringBuilder = new StringBuilder(result.get()); resultStringBuilder.append("/live/index"); if (isLocal()) { resultStringBuilder.append("_local"); } resultStringBuilder.append(".m3u8"); - return resultStringBuilder.toString(); + return Optional.of(resultStringBuilder.toString()); } - @SuppressWarnings("null") - protected String getVpnUrl() { - return (module == null) ? null : module.getVpnUrl(); + private Optional getVpnUrl() { + return getModule().map(NAWelcomeCamera::getVpnUrl); } - public String getStreamURL(String videoId) { - String result = getVpnUrl(); - if (result == null) { - return null; + public Optional getStreamURL(String videoId) { + Optional result = getVpnUrl(); + if (!result.isPresent()) { + return Optional.empty(); } - StringBuilder resultStringBuilder = new StringBuilder(result); + StringBuilder resultStringBuilder = new StringBuilder(result.get()); resultStringBuilder.append("/vod/"); resultStringBuilder.append(videoId); resultStringBuilder.append("/index"); @@ -151,11 +177,61 @@ public String getStreamURL(String videoId) { resultStringBuilder.append("_local"); } resultStringBuilder.append(".m3u8"); - return resultStringBuilder.toString(); + return Optional.of(resultStringBuilder.toString()); } - @SuppressWarnings("null") private boolean isLocal() { - return (module == null || module.getIsLocal() == null) ? false : module.getIsLocal(); + return getModule().map(NAWelcomeCamera::getIsLocal).orElse(false); + } + + private void switchVideoSurveillance(boolean isOn) { + Optional localCameraURL = getLocalCameraURL(); + if (localCameraURL.isPresent()) { + String url = localCameraURL.get() + STATUS_CHANGE_URL_PATH + "?status="; + if(isOn) { + url += "on"; + } else { + url += "off"; + } + executeGETRequest(url); + + invalidateParentCacheAndRefresh(); + } + } + + protected Optional getLocalCameraURL() { + Optional vpnURLOptional = getVpnUrl(); + if (vpnURLOptional.isPresent()) { + final String vpnURL = vpnURLOptional.get(); + + //The local address is (re-)requested when it wasn't already determined or when the vpn address was changed. + if (!cameraAddress.isPresent() || cameraAddress.get().isVpnURLChanged(vpnURL)) { + Optional json = executeGETRequestJSON(vpnURL + PING_URL_PATH); + cameraAddress = json.map(j -> j.optString("local_url", null)) + .map(localURL -> new CameraAddress(vpnURL, localURL)); + } + } + return cameraAddress.map(CameraAddress::getLocalURL); + } + + private Optional executeGETRequestJSON(String url) { + try { + return executeGETRequest(url).map(JSONObject::new); + } catch (JSONException e) { + logger.warn("Error on parsing the content as JSON!", e); + } + return Optional.empty(); + } + + protected Optional executeGETRequest(String url) { + try { + String content = HttpUtil.executeUrl("GET", url, 5000); + if (content != null && !content.isEmpty()) { + return Optional.of(content); + } + } catch (IOException e) { + logger.warn("Error on accessing local camera url!", e); + } + return Optional.empty(); } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoModuleHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoModuleHandler.java index d6a6b963c1a05..7d59e1bf9d67d 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoModuleHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoModuleHandler.java @@ -38,7 +38,7 @@ * @author Gaël L'hopital - Initial contribution */ public class NetatmoModuleHandler extends AbstractNetatmoThingHandler { - private Logger logger = LoggerFactory.getLogger(NetatmoModuleHandler.class); + private final Logger logger = LoggerFactory.getLogger(NetatmoModuleHandler.class); private ScheduledFuture refreshJob; @Nullable protected MODULE module; @@ -131,4 +131,8 @@ protected boolean isRefreshRequired() { protected void setRefreshRequired(boolean refreshRequired) { this.refreshRequired = refreshRequired; } + + protected @NonNull Optional getModule() { + return Optional.ofNullable(module); + } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandler.java index 133240b1d54f2..5c0250b852c6a 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandler.java @@ -24,14 +24,8 @@ import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.State; import org.eclipse.smarthome.core.types.UnDefType; -import org.eclipse.smarthome.io.net.http.HttpUtil; -import org.json.JSONException; -import org.json.JSONObject; import org.openhab.binding.netatmo.internal.camera.CameraHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.io.IOException; import java.util.Optional; import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT; @@ -40,17 +34,13 @@ /** * {@link NAPresenceCameraHandler} is the class used to handle Presence camera data * - * @author Sven Strohschein + * @author Sven Strohschein - Initial contribution */ @NonNullByDefault public class NAPresenceCameraHandler extends CameraHandler { - private static final String PING_URL_PATH = "/command/ping"; private static final String FLOODLIGHT_SET_URL_PATH = "/command/floodlight_set_config"; - private final Logger logger = LoggerFactory.getLogger(NAPresenceCameraHandler.class); - - private Optional cameraAddress = Optional.empty(); private State floodlightAutoModeState = UnDefType.UNDEF; public NAPresenceCameraHandler(final Thing thing, final TimeZoneProvider timeZoneProvider) { @@ -97,18 +87,15 @@ protected State getNAThingProperty(@NonNull String channelId) { } private State getFloodlightState() { - if (module != null) { - final boolean isOn = module.getLightModeStatus() == NAWelcomeCamera.LightModeStatusEnum.ON; - return toOnOffType(isOn); - } - return UnDefType.UNDEF; + return getModule() + .map(m -> toOnOffType(m.getLightModeStatus() == NAWelcomeCamera.LightModeStatusEnum.ON)) + .orElse(UnDefType.UNDEF); } private State getFloodlightAutoModeState() { - if (module != null) { - return toOnOffType(module.getLightModeStatus() == NAWelcomeCamera.LightModeStatusEnum.AUTO); - } - return UnDefType.UNDEF; + return getModule() + .map(m -> toOnOffType(m.getLightModeStatus() == NAWelcomeCamera.LightModeStatusEnum.AUTO)) + .orElse(UnDefType.UNDEF); } private void switchFloodlight(boolean isOn) { @@ -137,40 +124,8 @@ private void changeFloodlightMode(NAWelcomeCamera.LightModeStatusEnum mode) { + mode.toString() + "%22%7D"; executeGETRequest(url); - } - } - - private Optional getLocalCameraURL() { - String vpnURL = getVpnUrl(); - if (vpnURL != null) { - //The local address is (re-)requested when it wasn't already determined or when the vpn address was changed. - if (!cameraAddress.isPresent() || cameraAddress.get().isVpnURLChanged(vpnURL)) { - Optional json = executeGETRequestJSON(vpnURL + PING_URL_PATH); - cameraAddress = json.map(j -> j.optString("local_url", null)) - .map(localURL -> new CameraAddress(vpnURL, localURL)); - } - } - return cameraAddress.map(CameraAddress::getLocalURL); - } - - private Optional executeGETRequestJSON(String url) { - try { - return executeGETRequest(url).map(JSONObject::new); - } catch (JSONException e) { - logger.warn("Error on parsing the content as JSON!", e); - } - return Optional.empty(); - } - Optional executeGETRequest(String url) { - try { - String content = HttpUtil.executeUrl("GET", url, 5000); - if (content != null && !content.isEmpty()) { - return Optional.of(content); - } - } catch (IOException e) { - logger.warn("Error on accessing local camera url!", e); + invalidateParentCacheAndRefresh(); } - return Optional.empty(); } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java index 19b4ba0b81557..b37ee8134bc0c 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java @@ -147,9 +147,9 @@ protected State getNAThingProperty(String channelId) { Optional thing = getBridgeHandler().findNAThing(cameraId); if (thing.isPresent()) { CameraHandler eventCamera = (CameraHandler) thing.get(); - String streamUrl = eventCamera.getStreamURL(lastEvent.get().getVideoId()); - if (streamUrl != null) { - return new StringType(streamUrl); + Optional streamUrl = eventCamera.getStreamURL(lastEvent.get().getVideoId()); + if (streamUrl.isPresent()) { + return new StringType(streamUrl.get()); } } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/thing/camera.xml b/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/thing/camera.xml index be0698f6e5af5..d834b66ea5377 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/thing/camera.xml +++ b/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/thing/camera.xml @@ -56,7 +56,6 @@ Switch State of the camera - diff --git a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java index 977f754e232f8..d654c957b6723 100644 --- a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java +++ b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java @@ -37,7 +37,7 @@ import static org.mockito.Mockito.*; /** - * @author Sven Strohschein + * @author Sven Strohschein - Initial contribution */ @RunWith(MockitoJUnitRunner.class) public class NAPresenceCameraHandlerTest { @@ -53,6 +53,7 @@ public class NAPresenceCameraHandlerTest { private Thing presenceCameraThing; private NAWelcomeCamera presenceCamera; + private ChannelUID cameraStatusChannelUID; private ChannelUID floodlightChannelUID; private ChannelUID floodlightAutoModeChannelUID; private NAPresenceCameraHandlerAccessible handler; @@ -62,12 +63,50 @@ public void before() { presenceCameraThing = new ThingImpl(new ThingTypeUID("netatmo", "NOC"), "1"); presenceCamera = new NAWelcomeCamera(); + cameraStatusChannelUID = new ChannelUID(presenceCameraThing.getUID(), NetatmoBindingConstants.CHANNEL_CAMERA_STATUS); floodlightChannelUID = new ChannelUID(presenceCameraThing.getUID(), NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT); floodlightAutoModeChannelUID = new ChannelUID(presenceCameraThing.getUID(), NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE); handler = new NAPresenceCameraHandlerAccessible(presenceCameraThing, presenceCamera); } + @Test + public void testHandleCommand_Switch_Surveillance_on() { + when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE); + + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(cameraStatusChannelUID, OnOffType.ON); + + verify(requestExecutorMock, times(2)).executeGETRequest(any()); //1.) execute ping + 2.) execute switch on + verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/changestatus?status=on"); + } + + @Test + public void testHandleCommand_Switch_Surveillance_off() { + when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE); + + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(cameraStatusChannelUID, OnOffType.OFF); + + verify(requestExecutorMock, times(2)).executeGETRequest(any()); //1.) execute ping + 2.) execute switch off + verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/changestatus?status=off"); + } + + @Test + public void testHandleCommand_Switch_Surveillance_unknown_command() { + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(cameraStatusChannelUID, RefreshType.REFRESH); + + verify(requestExecutorMock, never()).executeGETRequest(any()); //nothing should get executed on a refresh command + } + + @Test + public void testHandleCommand_Switch_Surveillance_without_VPN() { + handler.handleCommand(cameraStatusChannelUID, OnOffType.ON); + + verify(requestExecutorMock, never()).executeGETRequest(any()); //nothing should get executed when no VPN address is set + } + @Test public void testHandleCommand_Switch_Floodlight_on() { when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE); @@ -334,9 +373,9 @@ public void testGetNAThingProperty_FloodlightAutoMode_Module_NULL() { @Test public void testGetStreamURL() { presenceCamera.setVpnUrl(DUMMY_VPN_URL); - String streamURL = handler.getStreamURL("dummyVideoId"); - assertNotNull(streamURL); - assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index.m3u8", streamURL); + Optional streamURL = handler.getStreamURL("dummyVideoId"); + assertTrue(streamURL.isPresent()); + assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index.m3u8", streamURL.get()); } @Test @@ -344,9 +383,9 @@ public void testGetStreamURL_local() { presenceCamera.setVpnUrl(DUMMY_VPN_URL); presenceCamera.setIsLocal(true); - String streamURL = handler.getStreamURL("dummyVideoId"); - assertNotNull(streamURL); - assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index_local.m3u8", streamURL); + Optional streamURL = handler.getStreamURL("dummyVideoId"); + assertTrue(streamURL.isPresent()); + assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index_local.m3u8", streamURL.get()); } @Test @@ -354,15 +393,15 @@ public void testGetStreamURL_not_local() { presenceCamera.setVpnUrl(DUMMY_VPN_URL); presenceCamera.setIsLocal(false); - String streamURL = handler.getStreamURL("dummyVideoId"); - assertNotNull(streamURL); - assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index.m3u8", streamURL); + Optional streamURL = handler.getStreamURL("dummyVideoId"); + assertTrue(streamURL.isPresent()); + assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index.m3u8", streamURL.get()); } @Test public void testGetStreamURL_without_VPN() { - String streamURL = handler.getStreamURL("dummyVideoId"); - assertNull(streamURL); + Optional streamURL = handler.getStreamURL("dummyVideoId"); + assertFalse(streamURL.isPresent()); } @Test @@ -404,7 +443,7 @@ private interface RequestExecutor { private class NAPresenceCameraHandlerAccessible extends NAPresenceCameraHandler { - public NAPresenceCameraHandlerAccessible(Thing thing, NAWelcomeCamera presenceCamera) { + private NAPresenceCameraHandlerAccessible(Thing thing, NAWelcomeCamera presenceCamera) { super(thing, timeZoneProviderMock); module = presenceCamera; } diff --git a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java index 683e77392e1f4..1ecd81ee5bd10 100644 --- a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java +++ b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java @@ -15,6 +15,8 @@ import io.swagger.client.model.NAWelcomeEvent; import io.swagger.client.model.NAWelcomeHome; import io.swagger.client.model.NAWelcomeHomeData; +import io.swagger.client.model.NAWelcomeSubEvent; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.library.types.StringType; import org.eclipse.smarthome.core.thing.Thing; @@ -31,14 +33,13 @@ import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEvent; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import static org.junit.Assert.*; import static org.mockito.Mockito.*; /** - * @author Sven Strohschein + * @author Sven Strohschein - Initial contribution */ @RunWith(MockitoJUnitRunner.class) public class NAWelcomeHomeHandlerTest { @@ -48,35 +49,20 @@ public class NAWelcomeHomeHandlerTest { @Mock private TimeZoneProvider timeZoneProviderMock; private Thing welcomeHomeThing; - private NAWelcomeHomeHandler handler; + private NAWelcomeHomeHandlerAccessible handler; @Mock private NetatmoBridgeHandler bridgeHandlerMock; @Before public void before() { welcomeHomeThing = new ThingImpl(new ThingTypeUID("netatmo", "NAWelcomeHome"), "1"); - handler = new NAWelcomeHomeHandler(welcomeHomeThing, timeZoneProviderMock) { - @Override - protected NetatmoBridgeHandler getBridgeHandler() { - return bridgeHandlerMock; - } - - @Override - protected String getId() { - return DUMMY_HOME_ID; - } - }; + handler = new NAWelcomeHomeHandlerAccessible(welcomeHomeThing); } @Test public void testUpdateReadings_with_Events() { - NAWelcomeEvent event_1 = new NAWelcomeEvent(); - event_1.setType(NAWebhookCameraEvent.EventTypeEnum.PERSON.toString()); - event_1.setTime(1592661881); - - NAWelcomeEvent event_2 = new NAWelcomeEvent(); - event_2.setType(NAWebhookCameraEvent.EventTypeEnum.MOVEMENT.toString()); - event_2.setTime(1592661882); + NAWelcomeEvent event_1 = createEvent(1592661881, NAWebhookCameraEvent.EventTypeEnum.PERSON); + NAWelcomeEvent event_2 = createEvent(1592661882, NAWebhookCameraEvent.EventTypeEnum.MOVEMENT); NAWelcomeHome home = new NAWelcomeHome(); home.setId(DUMMY_HOME_ID); @@ -95,13 +81,11 @@ public void testUpdateReadings_with_Events() { home.setEvents(Arrays.asList(event_2, event_1)); //the second (last) event is still expected (independent from the order of these are added) assertEquals(new StringType("movement"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); - } @Test public void testUpdateReadings_with_1_Event() { - NAWelcomeEvent event = new NAWelcomeEvent(); - event.setType(NAWebhookCameraEvent.EventTypeEnum.PERSON.toString()); + NAWelcomeEvent event = createEvent(1592661881, NAWebhookCameraEvent.EventTypeEnum.PERSON); NAWelcomeHome home = new NAWelcomeHome(); home.setId(DUMMY_HOME_ID); @@ -147,4 +131,99 @@ public void testUpdateReadings_no_HomeData() { assertEquals(UnDefType.UNDEF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); } + + @Test + public void testTriggerChannelIfRequired() { + NAWelcomeEvent event_1 = createPresenceEvent(1592661881, NAWelcomeSubEvent.TypeEnum.ANIMAL); + NAWelcomeEvent event_2 = createPresenceEvent(1592661882, NAWelcomeSubEvent.TypeEnum.HUMAN); + NAWelcomeEvent event_3 = createEvent(1592661883, NAWebhookCameraEvent.EventTypeEnum.MOVEMENT); + + NAWelcomeHome home = new NAWelcomeHome(); + home.setId(DUMMY_HOME_ID); + home.setEvents(Collections.singletonList(event_1)); + + NAWelcomeHomeData homeData = new NAWelcomeHomeData(); + homeData.setHomes(Collections.singletonList(home)); + + when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(homeData); + + handler.updateReadings(); + handler.triggerChannelIfRequired(NetatmoBindingConstants.CHANNEL_CAMERA_EVENT); + + //No triggered event is expected, because the binding is just started (with existing events). + assertEquals(0, handler.getTriggerChannelCount()); + + home.setEvents(Arrays.asList(event_1, event_2)); + + handler.updateReadings(); + handler.triggerChannelIfRequired(NetatmoBindingConstants.CHANNEL_CAMERA_EVENT); + + //1 triggered event is expected, because there is 1 new event since binding start (outdoor / detected human). + assertEquals(1, handler.getTriggerChannelCount()); + assertEquals(new StringType("outdoor"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + + home.setEvents(Arrays.asList(event_1, event_2)); + + handler.updateReadings(); + handler.triggerChannelIfRequired(NetatmoBindingConstants.CHANNEL_CAMERA_EVENT); + + //No new triggered event is expected, because there are still the same events as before the refresh. + assertEquals(1, handler.getTriggerChannelCount()); + assertEquals(new StringType("outdoor"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + + home.setEvents(Arrays.asList(event_1, event_2, event_3)); + + handler.updateReadings(); + handler.triggerChannelIfRequired(NetatmoBindingConstants.CHANNEL_CAMERA_EVENT); + + //1 new triggered event is expected (2 in sum), because there is 1 new event since the last triggered event (movement after outdoor / detected human). + assertEquals(2, handler.getTriggerChannelCount()); + assertEquals(new StringType("movement"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + } + + private static NAWelcomeEvent createPresenceEvent(int eventTime, NAWelcomeSubEvent.TypeEnum detectedObjectType) { + NAWelcomeSubEvent subEvent = new NAWelcomeSubEvent(); + subEvent.setTime(eventTime); + subEvent.setType(detectedObjectType); + + NAWelcomeEvent event = createEvent(eventTime, NAWebhookCameraEvent.EventTypeEnum.OUTDOOR); + event.setEventList(Collections.singletonList(subEvent)); + return event; + } + + private static NAWelcomeEvent createEvent(int eventTime, NAWebhookCameraEvent.EventTypeEnum eventType) { + NAWelcomeEvent event = new NAWelcomeEvent(); + event.setType(eventType.toString()); + event.setTime(eventTime); + return event; + } + + private class NAWelcomeHomeHandlerAccessible extends NAWelcomeHomeHandler { + + private int triggerChannelCount; + + private NAWelcomeHomeHandlerAccessible(Thing thing) { + super(thing, timeZoneProviderMock); + } + + @Override + protected NetatmoBridgeHandler getBridgeHandler() { + return bridgeHandlerMock; + } + + @Override + protected String getId() { + return DUMMY_HOME_ID; + } + + @Override + protected void triggerChannel(@NonNull String channelID, @NonNull String event) { + triggerChannelCount++; + super.triggerChannel(channelID, event); + } + + private int getTriggerChannelCount() { + return triggerChannelCount; + } + } } From 20c9f0e6aa457b002606ba66067755ae22ba8007 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Sun, 28 Jun 2020 22:31:49 +0200 Subject: [PATCH 17/85] [bosesoundtouch] fix possible resource leak (#8031) Signed-off-by: Jan N. Klug --- bundles/org.openhab.binding.bosesoundtouch/pom.xml | 4 +++- .../internal/handler/BoseSoundTouchHandler.java | 10 ++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.bosesoundtouch/pom.xml b/bundles/org.openhab.binding.bosesoundtouch/pom.xml index cada48d0ed878..d456c1cee955e 100644 --- a/bundles/org.openhab.binding.bosesoundtouch/pom.xml +++ b/bundles/org.openhab.binding.bosesoundtouch/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 diff --git a/bundles/org.openhab.binding.bosesoundtouch/src/main/java/org/openhab/binding/bosesoundtouch/internal/handler/BoseSoundTouchHandler.java b/bundles/org.openhab.binding.bosesoundtouch/src/main/java/org/openhab/binding/bosesoundtouch/internal/handler/BoseSoundTouchHandler.java index 49fc0a5029043..e3bf139c956c7 100644 --- a/bundles/org.openhab.binding.bosesoundtouch/src/main/java/org/openhab/binding/bosesoundtouch/internal/handler/BoseSoundTouchHandler.java +++ b/bundles/org.openhab.binding.bosesoundtouch/src/main/java/org/openhab/binding/bosesoundtouch/internal/handler/BoseSoundTouchHandler.java @@ -21,6 +21,7 @@ import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -93,6 +94,8 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket private PresetContainer presetContainer; private BoseStateDescriptionOptionProvider stateOptionProvider; + private Future sessionFuture; + /** * Creates a new instance of this class for the {@link Thing}. * @@ -423,7 +426,7 @@ private synchronized void openConnection() { request.setSubProtocols("gabbo"); client.setStopTimeout(1000); client.start(); - client.connect(this, new URI(wsUrl), request); + sessionFuture = client.connect(this, new URI(wsUrl), request); } catch (Exception e) { onWebSocketError(e); } @@ -439,7 +442,10 @@ private synchronized void closeConnection() { } session = null; } - if (client != null) { + if (sessionFuture != null && !sessionFuture.isDone()) { + sessionFuture.cancel(true); + } + if (client != null && client.isStarted()) { try { client.stop(); client.destroy(); From 09c93c58cab564f5b4eca8bd297f257772fc761a Mon Sep 17 00:00:00 2001 From: J-N-K Date: Sun, 28 Jun 2020 22:33:21 +0200 Subject: [PATCH 18/85] [kodi] fix possible resource leak (#8030) Signed-off-by: Jan N. Klug --- bundles/org.openhab.binding.kodi/pom.xml | 4 +++- .../binding/kodi/internal/protocol/KodiClientSocket.java | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.kodi/pom.xml b/bundles/org.openhab.binding.kodi/pom.xml index c9e7d9fff6d85..8775aab30ea90 100644 --- a/bundles/org.openhab.binding.kodi/pom.xml +++ b/bundles/org.openhab.binding.kodi/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 diff --git a/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiClientSocket.java b/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiClientSocket.java index 3b6c5dc4f04da..757e0dfa500e8 100644 --- a/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiClientSocket.java +++ b/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiClientSocket.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.net.URI; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -58,6 +59,7 @@ public class KodiClientSocket { private final URI uri; private final WebSocketClient client; private Session session; + private Future sessionFuture; private final KodiClientSocketEventListener eventHandler; @@ -81,7 +83,7 @@ public synchronized void open() throws IOException { KodiWebSocketListener socket = new KodiWebSocketListener(); ClientUpgradeRequest request = new ClientUpgradeRequest(); - client.connect(socket, uri, request); + sessionFuture = client.connect(socket, uri, request); } /*** @@ -93,6 +95,10 @@ public void close() { session.close(); session = null; } + + if (sessionFuture != null && !sessionFuture.isDone()) { + sessionFuture.cancel(true); + } } public boolean isConnected() { From 956977da0c45e505bc9e1a076bba679607920508 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Sun, 28 Jun 2020 22:34:57 +0200 Subject: [PATCH 19/85] [tibber] fix possible resource leak (#8033) Signed-off-by: Jan N. Klug --- bundles/org.openhab.binding.tibber/pom.xml | 4 +++- .../tibber/internal/handler/TibberHandler.java | 14 +++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.tibber/pom.xml b/bundles/org.openhab.binding.tibber/pom.xml index 6b6b76244dbb7..fc8925de66143 100644 --- a/bundles/org.openhab.binding.tibber/pom.xml +++ b/bundles/org.openhab.binding.tibber/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 diff --git a/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/handler/TibberHandler.java b/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/handler/TibberHandler.java index e9cfa029829e9..7317b5f89e35b 100755 --- a/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/handler/TibberHandler.java +++ b/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/handler/TibberHandler.java @@ -21,6 +21,7 @@ import java.net.URISyntaxException; import java.util.Properties; import java.util.concurrent.Executor; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -75,6 +76,7 @@ public class TibberHandler extends BaseThingHandler { private @Nullable Session session; private @Nullable WebSocketClient client; private @Nullable ScheduledFuture pollingJob; + private @Nullable Future sessionFuture; private String rtEnabled = "false"; public TibberHandler(Thing thing) { @@ -326,7 +328,7 @@ public void open() { } try { logger.debug("Connecting Websocket connection"); - client.connect(socket, new URI(SUBSCRIPTION_URL), newRequest); + sessionFuture = client.connect(socket, new URI(SUBSCRIPTION_URL), newRequest); } catch (IOException e) { logger.warn("Websocket Connect Exception: {}", e.getMessage()); } catch (URISyntaxException e) { @@ -354,6 +356,16 @@ public void close() { this.session = null; this.socket = null; } + if (sessionFuture != null && !sessionFuture.isDone()) { + sessionFuture.cancel(true); + } + if (client != null && client.isStarted()) { + try { + client.stop(); + } catch (Exception e) { + logger.warn("Failed to stop websocket client: {}", e.getMessage()); + } + } } public boolean isConnected() { From 8a5c07f05f4b1dd5108543a6031375741d560f49 Mon Sep 17 00:00:00 2001 From: Fabio Possieri Date: Sun, 28 Jun 2020 23:01:00 +0200 Subject: [PATCH 20/85] [bticinosmarther] Initial contribution (#7533) Signed-off-by: Fabio Possieri --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../.classpath | 32 + .../.project | 23 + .../NOTICE | 17 + .../README.md | 322 ++++++++ .../doc/images/application-1.png | Bin 0 -> 17459 bytes .../doc/images/application-2.png | Bin 0 -> 10547 bytes .../doc/images/tutorial-1.png | Bin 0 -> 48619 bytes .../doc/images/tutorial-2.png | Bin 0 -> 19477 bytes .../doc/images/tutorial-3.png | Bin 0 -> 10128 bytes .../pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../internal/SmartherBindingConstants.java | 114 +++ .../account/SmartherAccountHandler.java | 224 +++++ .../account/SmartherAccountService.java | 292 +++++++ .../account/SmartherAuthorizationServlet.java | 272 +++++++ .../account/SmartherNotificationHandler.java | 66 ++ .../account/SmartherNotificationServlet.java | 132 +++ .../internal/api/SmartherApi.java | 505 ++++++++++++ .../internal/api/SmartherApiConnector.java | 336 ++++++++ .../internal/api/dto/Chronothermostat.java | 234 ++++++ .../internal/api/dto/Enums.java | 268 ++++++ .../internal/api/dto/Location.java | 185 +++++ .../internal/api/dto/Measure.java | 120 +++ .../internal/api/dto/Module.java | 63 ++ .../internal/api/dto/ModuleRef.java | 38 + .../internal/api/dto/ModuleStatus.java | 51 ++ .../internal/api/dto/Modules.java | 56 ++ .../internal/api/dto/Notification.java | 112 +++ .../internal/api/dto/Plant.java | 62 ++ .../internal/api/dto/PlantRef.java | 48 ++ .../internal/api/dto/Plants.java | 35 + .../internal/api/dto/Program.java | 50 ++ .../internal/api/dto/Sender.java | 61 ++ .../internal/api/dto/Sensor.java | 70 ++ .../internal/api/dto/Subscription.java | 63 ++ .../internal/api/dto/Topology.java | 48 ++ .../SmartherAuthorizationException.java | 49 ++ .../exception/SmartherGatewayException.java | 64 ++ ...SmartherIllegalPropertyValueException.java | 53 ++ .../SmartherInvalidResponseException.java | 37 + .../SmartherNotificationException.java | 49 ++ ...herSubscriptionAlreadyExistsException.java | 37 + .../SmartherTokenExpiredException.java | 37 + .../config/SmartherBridgeConfiguration.java | 201 +++++ .../config/SmartherModuleConfiguration.java | 146 ++++ .../SmartherModuleDiscoveryService.java | 176 ++++ .../factory/SmartherHandlerFactory.java | 94 +++ .../handler/SmartherBridgeHandler.java | 766 ++++++++++++++++++ ...artherDynamicStateDescriptionProvider.java | 74 ++ .../handler/SmartherModuleHandler.java | 735 +++++++++++++++++ .../internal/model/BridgeStatus.java | 128 +++ .../internal/model/ModuleSettings.java | 315 +++++++ .../internal/util/DateUtil.java | 181 +++++ .../internal/util/ModelUtil.java | 45 + .../internal/util/StringUtil.java | 179 ++++ .../resources/ESH-INF/binding/binding.xml | 10 + .../main/resources/ESH-INF/config/config.xml | 141 ++++ .../resources/ESH-INF/thing/bridge-types.xml | 101 +++ .../resources/ESH-INF/thing/thing-types.xml | 221 +++++ .../main/resources/templates/application.html | 4 + .../src/main/resources/templates/index.html | 95 +++ .../src/main/resources/web/favicon.ico | Bin 0 -> 32038 bytes bundles/pom.xml | 5 +- 65 files changed, 7873 insertions(+), 1 deletion(-) create mode 100644 bundles/org.openhab.binding.bticinosmarther/.classpath create mode 100644 bundles/org.openhab.binding.bticinosmarther/.project create mode 100644 bundles/org.openhab.binding.bticinosmarther/NOTICE create mode 100644 bundles/org.openhab.binding.bticinosmarther/README.md create mode 100644 bundles/org.openhab.binding.bticinosmarther/doc/images/application-1.png create mode 100644 bundles/org.openhab.binding.bticinosmarther/doc/images/application-2.png create mode 100644 bundles/org.openhab.binding.bticinosmarther/doc/images/tutorial-1.png create mode 100644 bundles/org.openhab.binding.bticinosmarther/doc/images/tutorial-2.png create mode 100644 bundles/org.openhab.binding.bticinosmarther/doc/images/tutorial-3.png create mode 100644 bundles/org.openhab.binding.bticinosmarther/pom.xml create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/SmartherBindingConstants.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherAccountHandler.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherAccountService.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherAuthorizationServlet.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherNotificationHandler.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherNotificationServlet.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/SmartherApi.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/SmartherApiConnector.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Chronothermostat.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Enums.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Location.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Measure.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Module.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/ModuleRef.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/ModuleStatus.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Modules.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Notification.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Plant.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/PlantRef.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Plants.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Program.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Sender.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Sensor.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Subscription.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Topology.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherAuthorizationException.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherGatewayException.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherIllegalPropertyValueException.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherInvalidResponseException.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherNotificationException.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherSubscriptionAlreadyExistsException.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherTokenExpiredException.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/config/SmartherBridgeConfiguration.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/config/SmartherModuleConfiguration.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/discovery/SmartherModuleDiscoveryService.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/factory/SmartherHandlerFactory.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/handler/SmartherBridgeHandler.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/handler/SmartherDynamicStateDescriptionProvider.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/handler/SmartherModuleHandler.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/model/BridgeStatus.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/model/ModuleSettings.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/util/DateUtil.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/util/ModelUtil.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/util/StringUtil.java create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/config/config.xml create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/thing/bridge-types.xml create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/resources/templates/application.html create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/resources/templates/index.html create mode 100644 bundles/org.openhab.binding.bticinosmarther/src/main/resources/web/favicon.ico diff --git a/CODEOWNERS b/CODEOWNERS index 83ddb10991300..2fb763c319f9a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -28,6 +28,7 @@ /bundles/org.openhab.binding.boschindego/ @jofleck /bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho /bundles/org.openhab.binding.bsblan/ @hypetsch +/bundles/org.openhab.binding.bticinosmarther/ @MrRonfo /bundles/org.openhab.binding.buienradar/ @gedejong /bundles/org.openhab.binding.chromecast/ @kaikreuzer /bundles/org.openhab.binding.cm11a/ @BobRak diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index e7923e55d499d..c7b20c2a3e5ec 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -129,6 +129,11 @@ org.openhab.binding.bsblan ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.bticinosmarther + ${project.version} + org.openhab.addons.bundles org.openhab.binding.buienradar diff --git a/bundles/org.openhab.binding.bticinosmarther/.classpath b/bundles/org.openhab.binding.bticinosmarther/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.bticinosmarther/.project b/bundles/org.openhab.binding.bticinosmarther/.project new file mode 100644 index 0000000000000..8ddf780f2d296 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.bticinosmarther + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.bticinosmarther/NOTICE b/bundles/org.openhab.binding.bticinosmarther/NOTICE new file mode 100644 index 0000000000000..2c474b712818c --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/NOTICE @@ -0,0 +1,17 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons + +Some code of this bundle originates from or was inspired by: +* Tim Waterhouse (Daikin binding) +* Hilbrand Bouwkamp (Spotify binding) diff --git a/bundles/org.openhab.binding.bticinosmarther/README.md b/bundles/org.openhab.binding.bticinosmarther/README.md new file mode 100644 index 0000000000000..b925169ddb5d1 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/README.md @@ -0,0 +1,322 @@ +# BTicinoSmarther Binding + +The BTicino Smarther binding implements a bridge to the Legrand/BTicino Smarther v2.0 API and allows you to control your BTicino Smarther chronothermostat units with openHAB, making it possible to discover Smarther units connected to your Legrand/BTicino Smarther account. + +Smarther chronothermostat is produced by [BTicino](https://www.bticino.com/products-catalogue/smarther-the-connected-thermostat/), has its own API set and does not support the OpenWebNet protocol. + +## Supported Things + +All BTicino Smarther Chronothermostat device models should be discoverable through this binding: + +* Flush mounting installation item (X8000) +* Wall installation item (X8000W) + +If you can control them from BTicino Thermostat mobile app on your iPhone/Android you should be able to add it as a thing. + +## Discovery + +As long as BTicino Smarther Chronothermostat devices are available in the locations registered on the user account configured with the bridge they should show up whenever you initiate discovery of things. + +If no devices are showing up, try to connect to the device(s) from your smartphone to make sure the device(s) are in use by your user account. + +The discovery of devices in the Smarther API is based on what is known by Legrand. +There is difference between e.g. smartphones and computers which can discover devices on the local network and the Smarther API which is not able to do so; it only knows about a device if your account is currently associated with the device. + +## Thing Configuration + +Each Bridge item requires you to register an Application with [Legrand Developer portal](https://developer.legrand.com). +This will get you a set of Client ID and Client Secret parameters to be used in the [Bridge Configuration](#bridge-configuration) phase. + +Optionally, if you want to later receive push notifications on the status of your units, consider to make your openHAB installation reachable in https from a public IP or domain (see [Note on notifications](#note-on-notifications)). + +The following configuration options are available on the BTicino Smarther Bridge thing: + +| Parameter | Description | Condition | +|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|-----------| +| subscriptionKey | This is the Primary Key provided by Legrand when you create a new Account and subscribe to a product | Required | +| clientId | This is the Client ID provided by Legrand when you add a new Application for openHAB to your Legrand account | Required | +| clientSecret | This is the Client Secret provided by Legrand when you add a new Application for openHAB to your Legrand account | Required | +| useNotifications | ON = the bridge subscribes to receive push notifications on devices status change; OFF = status updates are requested (pull) on a periodical basis | | +| statusRefreshPeriod | This is the frequency of the polling requests to the Smarther API to update the bridge status (in minutes) | | + +The following configuration options are available on the BTicino Smarther Chronothermostat thing: + +| Parameter | Description | Condition | +|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|-----------| +| plantId | This is the Plant Id of the location the Chronothermostat module is installed in, provided by Smarther API | Read-only | +| moduleId | This is the Module Id of the Chronothermostat module, provided by Smarther API | Read-only | +| settingsAutoupdate | ON = the thermostat settings are aligned with current status whenever it changes; OFF = settings are aligned to status only upon initialization | | +| programsRefreshPeriod | This is the frequency the Smarther API gateway is called to refresh Programs list used in "automatic" mode (in hours) | | +| numberOfEndDays | This is the number of days to be displayed in module settings, as options list for "End Date" field in "manual" mode | | +| statusRefreshPeriod | This is the frequency of the polling requests to the Smarther API to update the module status and sensor data (in minutes) | | + +### Account Creation + +Follow the instructions in the tutorial [here](https://developer.legrand.com/tutorials/getting-started/), under: + +* Step 1 : Create an account +* Step 2 : Subscribe to a product and get subscription key + +There's also a Step 3 in this tutorial, you can skip it as not needed to complete this process. +Simply write down your "Primary Key" as it will be needed later on in the bridge configuration phase. + +### Application Creation + +Follow the instructions in the tutorial [here](https://developer.legrand.com/tutorials/create-an-application/), under: + +* Step 1 : Register your application +* Step 2 : Check scopes +* Step 3 : Getting application details + +When registering your new Legrand Application for openHAB BTicino Smarther Bridge you have to specify the allowed Reply URL, aka white-listed address. +Here you have to specify the URL to the Bridge Authorization page on your server. + +For example if you run your openHAB server on http://openhabianpi:8080 you should set `http://openhabianpi:8080/bticinosmarther/connectsmarther` as the "First Reply URL" required field in Step 1. +Other Reply URLs (second, third, etc.) you can leave them blank. + +This is **very important** since the authorize process with Legrand takes place using your client web browser and Legrand will have to know the right URL to your openHAB server for the authorization to be completed. +When you have authorized with Legrand, this Redirect URI is where authorization tokens for your openHAB BTicino Smarther Brigde will be sent and they have to be received by the servlet on `/bticinosmarther/connectsmarther`. + +![Application 1](doc/images/application-1.png) + +On Step 2, please make sure to select both `comfort.read` and `comfort.write` scopes, as they're mandatory for the bridge to manage its chronothermostat devices. + +![Application 2](doc/images/application-2.png) + +Usually, Step 3 is then completed by Legrand within 1-2 days and you'll receive an email containing your application's Client ID and Client Secret. + +### Note On Notifications + +If you want to later receive push notifications (device status) from Legrand for this application, you must have your openHAB server reachable from a public IP/address and use the related public IP/address and public port when filling-in the "Reply URL" field in Step 1. + +### Bridge Configuration + +1. Install the binding and make sure the _BTicino Smarther Binding_ is listed on your server, if you have not already done so. +2. Complete the [Account Creation](#account-creation) and [Application Creation](#application-creation) steps, if you have not already done so. +3. Make sure you have your Legrand account _Primary Key_ and your Legrand application _Client ID_ and _Client Secret_ identities available. +4. Go to your preferred openHAB admin UI and add a new Thing - select the **"BTicino Smarther Bridge"**. +5. Choose new Id for the bridge, unless you like the generated one. +6. Put in your _Primary Key_ (in _Subscription Key_ field), _Client ID_ and _Cliend Secret_ in their respective fields of the bridge configuration. +7. Set _Use Notifications_ to `ON` if your openHAB server is reachable from a public https URL (see [Note on notifications](#note-on-notifications)), set `OFF` otherwise. +8. You can leave the _Bridge Status Refresh Period_ as is. +9. Save the bridge. +10. The bridge thing will stay in state _INITIALIZING_ and eventually go _OFFLINE_ - this is fine, as you now have to authorize this bridge with Legrand. +11. Go to the authorization page of your server (see [Application Creation](#application-creation)) `http://:/bticinosmarther/connectsmarther`; your newly added bridge should be listed there (along with the available locations). +12. Press the _"Authorize Bridge"_ button; this will take you either to the login page of Legrand portal or directly to the authorization screen. +13. Login and/or authorize the application; if the Reply URL is correct you will be returned and the entry should show your bridge is authorized with your Client ID; otherwise, go back to your application configuration on Legrand portal and ensure you have set the right Reply URL (see [Troubleshooting](#troubleshooting) below). +14. The bridge will be updated with a refresh token and go _ONLINE_ (the refresh token is used to re-authorize the bridge with Legrand Smarther API whenever required). + +![Tutorial 1](doc/images/tutorial-1.png) + +![Tutorial 2](doc/images/tutorial-2.png) + +![Tutorial 3](doc/images/tutorial-3.png) + +Now that you have got your bridge _ONLINE_ it is time to discover your devices! Go to Paper UI Inbox and search for **"BTicino Smarther Chronothermostat"** things. +Any BTicino Smarther Chronothermostat device currently available on your account should show up immediately. + +If no devices show up you may have to trigger the openHAB discovery several times as bridge will only find active devices known by the Smarther API at the time the discovery is triggered. + +Should the bridge configuration be broken for any reason, the authorization procedure can be reinitiated from step 11 whenever required. +You can force reinitialization by authorizing again on the `/bticinosmarther/connectsmarther` page, even if the page shows it as already authorized. This will reset the refresh token. + +### Troubleshooting + +When configuring the bridge (see step 13 [here](#thing-configuration)), you can receive the following error from Legrand portal: + +``` +{ + "error": "invalid_request", + "error_description": "The reply url host xxxxx doesn't match with the ones configured on the application" +} +``` + +This means you've either opened the `/bticinosmarther/connectsmarther` page from the wrong address or set the wrong "Reply URL" attribute in your application (see step 1 [here](#application-creation)). +Please remember these two strings must match for authentication process to work. + +To solve the issue, either: + +* Correct the address you're accessing the `/bticinosmarther/connectsmarther` page from, to match the "Reply URL" attribute registered in your application, or +* Should you have specified a wrong "Reply URL" attribute in your application, go to the Legrand portal and correct it accordingly then resubmit the application for approval. + +## Channels + +### Bridge + +The channels on the bridge are the ones used to get details of current communication with Smarther API on the Legrand account associated with the bridge. + +**Status Channels:** + +The following channels represent the current operational status of the bridge and must all be referenced with the `status#` prefix. + +| Channel Type ID | Item Type | Read/Write | Description | Type | +|-------------------|----------------------|------------|------------------------------------------------------------------------------------------------------------------|----------| +| apiCallsHandled | Number | Read-only | The total number of API calls handled by the bridge | Common | +| notifsReceived | Number | Read-only | The total number of push notifications received by the bridge | Common | +| notifsRejected | Number | Read-only | The total number of push notifications rejected by the bridge (part of the received ones) | Common | + +**Configuration Channels:** + +The following channels represent convenience configuration channels for the bridge and must all be referenced with the `config#` prefix. + +| Channel Type ID | Item Type | Read/Write | Description | Type | +|-------------------|----------------------|------------|------------------------------------------------------------------------------------------------------------------|----------| +| fetchLocations | Switch | Read-write | Trigger to manually fetch the updated client locations list from Smarther API | Advanced | + +### Devices + +**Measures Channels:** + +The following channels represent the measures taken from the module on-board sensors and must all be referenced with the `measures#` prefix. + +| Channel Type ID | Item Type | Read/Write | Description | Type | +|-------------------|----------------------|------------|------------------------------------------------------------------------------------------------------------------|----------| +| temperature | Number:Temperature | Read-only | Indoor temperature as measured by the sensor (precision of 1/10 degree Celsius) | Common | +| humidity | Number:Dimensionless | Read-only | Indoor humidity as measured by the sensor (in percentage) | Common | + +**Status Channels:** + +The following channels represent the current operational status of the module and must all be referenced with the `status#` prefix. + +| Channel Type ID | Item Type | Read/Write | Description | Type | +|-------------------|----------------------|------------|------------------------------------------------------------------------------------------------------------------|----------| +| state | Switch | Read-only | Current operational state of the module | Common | +| function | String | Read-only | Current operational function set on the module (HEATING, COOLING) | Advanced | +| mode | String | Read-only | Current operational mode set on the module (AUTOMATIC, MANUAL, BOOST, OFF, PROTECTION) | Common | +| temperature | Number:Temperature | Read-only | Current operational target temperature set on the module (precision of 1/10 degree Celsius) | Common | +| program | String | Read-only | Current operational program set on the module (valid only for "Automatic" mode) | Common | +| endTime | String | Read-only | Current operational end time set on the module | Common | +| temperatureFormat | String | Read-only | Current operational temperature format of the module | Advanced | + +**Settings Channels:** + +The following channels represent the new operational settings to be applied to the module and must all be referenced with the `settings#` prefix. + +| Channel Type ID | Item Type | Read/Write | Description | Type | +|-------------------|----------------------|------------|------------------------------------------------------------------------------------------------------------------|----------| +| mode | String | Read-write | New operational mode to be set (AUTOMATIC, MANUAL, BOOST, OFF, PROTECTION) | Common | +| temperature | Number:Temperature | Read-write | New operational set-point temperature to be set (valid only for "Manual" mode, precision of 1/10 degree Celsius) | Common | +| program | Number | Read-write | New operational program to be set (valid only for "Automatic" mode) | Common | +| boostTime | Number | Read-write | New operational boost time to be set (valid only for "Boost" mode) | Common | +| endDate | String | Read-write | New operational end date to be set (valid only for "Manual" mode) | Common | +| endHour | Number | Read-write | New operational end hour to be set (valid only for "Manual" mode) | Common | +| endMinute | Number | Read-write | New operational end minute to be set (valid only for "Manual" mode) | Common | +| power | Switch | Read-write | Power on, send new operational settings to the module | Common | + +_**Note:**_ The `program` and `endDate` channels are Selection channels. +They are dynamically populated, respectively with the module specific set programs and the next N days (starting from _"Today"_, N driven by the `numberOfEndDays` device configuration option). + +**Configuration Channels:** + +The following channels represent convenience configuration channels for the module and must all be referenced with the `config#` prefix. + +| Channel Type ID | Item Type | Read/Write | Description | Type | +|-------------------|----------------------|------------|------------------------------------------------------------------------------------------------------------------|----------| +| fetchPrograms | Switch | Read-write | Trigger to manually fetch the updated module programs list from Smarther API | Advanced | + +## Full Example + +In this example there is a bridge configured with Thing ID **mybridge**: + +bticinosmarther.things: + +``` +Bridge bticinosmarther:bridge:mybridge "BTicino Smarther Bridge" [subscriptionKey="" clientId="", clientSecret=""] { + Thing module thermo1 "Thermo 1" [plantId="" moduleId=""] + Thing module thermo2 "Thermo 2" [plantId="" moduleId=""] +} +``` + +bticinosmarther.items: + +``` +// Measures items +Number:Temperature smaTemperature "In Temperature [%.1f %unit%]" { channel="bticinosmarther:module:mybridge:thermo1:measures#temperature" } +Number:Dimensionless smaHumidity "In Humidity [%.1f %unit%]" { channel="bticinosmarther:module:mybridge:thermo1:measures#humidity" } + +// Status items +Switch smaCurState "Cur State [%s]" { channel="bticinosmarther:module:mybridge:thermo1:status#state" } +String smaCurMode "Cur Mode [%s]" { channel="bticinosmarther:module:mybridge:thermo1:status#mode" } +Number:Temperature smaCurPoint "Cur Temperature [%.1f %unit%]" { channel="bticinosmarther:module:mybridge:thermo1:status#temperature" } +String smaCurProgram "Cur Program [%s]" { channel="bticinosmarther:module:mybridge:thermo1:status#program" } +String smaCurTime "Cur Timer [%s]" { channel="bticinosmarther:module:mybridge:thermo1:status#endTime" } + +// Settings items +String smaSetMode "Set Mode [%s]" { channel="bticinosmarther:module:mybridge:thermo1:settings#mode" } +Number:Temperature smaSetPoint "Set Temperature [%.0f %unit%]" { channel="bticinosmarther:module:mybridge:thermo1:settings#temperature" } +Number smaSetProgram "Set Program [%s]" { channel="bticinosmarther:module:mybridge:thermo1:settings#program" } +Number smaSetBoost "Set Boost Time []" { channel="bticinosmarther:module:mybridge:thermo1:settings#boostTime" } +String smaSetDate "Set End Date [%s]" { channel="bticinosmarther:module:mybridge:thermo1:settings#endDate" } +Number smaSetHour "Set End Hour [%02d:00]" { channel="bticinosmarther:module:mybridge:thermo1:settings#endHour" } +Number smaSetMinute "Set End Minute [hh:%02d]" { channel="bticinosmarther:module:mybridge:thermo1:settings#endMinute" } +Switch smaSetPower "Apply Changes" { channel="bticinosmarther:module:mybridge:thermo1:settings#power" } + +// Convenience items +Switch smaDisplayTime +``` + +bticinosmarther.sitemap: + +``` +sitemap bticinosmarther label="BTicino Smarther Sitemap" { + + Frame label="Smarther Measures" { + Text item=smaTemperature + Text item=smaHumidity + } + + Frame label="Smarther Status" { + Text item=smaCurState + Text item=smaCurMode + Text item=smaCurPoint + Text item=smaCurProgram + Text item=smaCurTime + } + + Frame label="Smarther Settings" { + Selection item=smaSetMode + Selection item=smaSetProgram visibility=[smaSetMode=="AUTOMATIC"] + Setpoint item=smaSetPoint minValue=7 maxValue=24 step=1 visibility=[smaSetMode=="MANUAL"] + Selection item=smaSetDate visibility=[smaSetMode=="MANUAL"] + Setpoint item=smaSetHour minValue=0 maxValue=23 step=1 visibility=[smaDisplayTime==ON] + Setpoint item=smaSetMinute minValue=0 maxValue=45 step=15 visibility=[smaDisplayTime==ON] + Switch item=smaSetBoost mappings=[30="30'", 60="60'", 90="90'"] visibility=[smaSetMode=="BOOST"] + Switch item=smaSetPower mappings=["ON"="Start"] + } +} +``` + +bticinosmarther.rules: + +``` +rule "BTicino Smarther - Set time visibility flag" +when + Item smaSetMode received update or Item smaSetDate received update +then + if (smaSetMode.state == "MANUAL" && smaSetDate.state != "") { + smaDisplayTime.sendCommand(ON) + } + else { + smaDisplayTime.sendCommand(OFF) + } +end +``` + +## Binding Model And Smarther API + +The model of the binding is such that the bridge takes care of all the remote communications with the Smarther API in the context of a specific user. +All devices (chronothermostats modules) currently associated with the user account are available to control. + +Legrand/BTicino Smarther topology considers the following dimensions: + +* topology : is the whole network of devices associated with a user account +* plant : is the location where a module is installed (0..N plants per topology) +* module : is the chronothermostat installed in a location (0..N modules per location) + +You can add multiple bridges to allow controlling devices in the context of multiple Legrand user accounts. + +Legrand manages the push notifications on device status via MS Azure C2C queues. +Each bridge registers itself to the queue, takes care of incoming notifications on behalf of its managed devices and dispatches each payload to the related device. + +The binding uses the [Smarther API v2.0](https://portal.developer.legrand.com/docs/services/smartherV2/operations/Chronothermostat-Measures). +The main gap between the API and Smarther mobile app is that the latter also allows you to create new programs. diff --git a/bundles/org.openhab.binding.bticinosmarther/doc/images/application-1.png b/bundles/org.openhab.binding.bticinosmarther/doc/images/application-1.png new file mode 100644 index 0000000000000000000000000000000000000000..5f5cdefdc31cc2df11302b1139245955be6acee3 GIT binary patch literal 17459 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.bticinosmarther/doc/images/application-2.png b/bundles/org.openhab.binding.bticinosmarther/doc/images/application-2.png new file mode 100644 index 0000000000000000000000000000000000000000..5a31dc8de85db9d393ca88a92ee360ce8416ba0c GIT binary patch literal 10547 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 F{{=e^>E-|c literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.bticinosmarther/doc/images/tutorial-2.png b/bundles/org.openhab.binding.bticinosmarther/doc/images/tutorial-2.png new file mode 100644 index 0000000000000000000000000000000000000000..04c2ad8c7902f2c931f8622001c674e4c837556c GIT binary patch literal 19477 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 + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.7-SNAPSHOT + + + org.openhab.binding.bticinosmarther + + openHAB Add-ons :: Bundles :: BTicino Smarther Binding + + diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/feature/feature.xml b/bundles/org.openhab.binding.bticinosmarther/src/main/feature/feature.xml new file mode 100644 index 0000000000000..d0dd56d6f9c76 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.bticinosmarther/${project.version} + + diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/SmartherBindingConstants.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/SmartherBindingConstants.java new file mode 100644 index 0000000000000..2fa2e2cbdd579 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/SmartherBindingConstants.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The {@code SmartherBindingConstants} class defines the common constants used across the whole binding. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherBindingConstants { + + private static final String BINDING_ID = "bticinosmarther"; + + // Date and time formats used by the binding + public static final String DTF_DATE = "dd/MM/yyyy"; + public static final String DTF_DATETIME = "yyyy-MM-dd'T'HH:mm:ss"; + public static final String DTF_DATETIME_EXT = "yyyy-MM-dd'T'HH:mm:ssXXX"; + public static final String DTF_TODAY = "'Today at' HH:mm"; + public static final String DTF_TOMORROW = "'Tomorrow at' HH:mm"; + public static final String DTF_DAY_HHMM = "dd/MM/yyyy 'at' HH:mm"; + + // Generic constants + public static final String HTTPS_SCHEMA = "https"; + public static final String NAME_SEPARATOR = ", "; + public static final String UNAVAILABLE = "N/A"; + public static final String DEFAULT_PROGRAM = "Default"; + + // List of BTicino/Legrand API gateway related urls, information + public static final String SMARTHER_ACCOUNT_URL = "https://partners-login.eliotbylegrand.com"; + public static final String SMARTHER_AUTHORIZE_URL = SMARTHER_ACCOUNT_URL + "/authorize"; + public static final String SMARTHER_API_TOKEN_URL = SMARTHER_ACCOUNT_URL + "/token"; + public static final String SMARTHER_API_SCOPES = Stream.of("comfort.read", "comfort.write") + .collect(Collectors.joining(" ")); + public static final String SMARTHER_API_URL = "https://api.developer.legrand.com/smarther/v2.0"; + + // Servlets and resources aliases + public static final String AUTH_SERVLET_ALIAS = "/" + BINDING_ID + "/connectsmarther"; + public static final String NOTIFY_SERVLET_ALIAS = "/" + BINDING_ID + "/notifysmarther"; + public static final String IMG_SERVLET_ALIAS = "/img"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge"); + public static final ThingTypeUID THING_TYPE_MODULE = new ThingTypeUID(BINDING_ID, "module"); + + // List of all common properties + public static final String PROPERTY_STATUS_REFRESH_PERIOD = "statusRefreshPeriod"; + + // List of all bridge properties + public static final String PROPERTY_SUBSCRIPTION_KEY = "subscriptionKey"; + public static final String PROPERTY_CLIENT_ID = "clientId"; + public static final String PROPERTY_CLIENT_SECRET = "clientSecret"; + public static final String PROPERTY_NOTIFICATION_URL = "notificationUrl"; + public static final String PROPERTY_NOTIFICATIONS = "notifications"; + + // List of all module properties + public static final String PROPERTY_PLANT_ID = "plantId"; + public static final String PROPERTY_MODULE_ID = "moduleId"; + public static final String PROPERTY_MODULE_NAME = "moduleName"; + public static final String PROPERTY_DEVICE_TYPE = "deviceType"; + + // List of all bridge Status Channel ids + public static final String CHANNEL_STATUS_API_CALLS_HANDLED = "status#apiCallsHandled"; + public static final String CHANNEL_STATUS_NOTIFS_RECEIVED = "status#notifsReceived"; + public static final String CHANNEL_STATUS_NOTIFS_REJECTED = "status#notifsRejected"; + // List of all bridge Config Channel ids + public static final String CHANNEL_CONFIG_FETCH_LOCATIONS = "config#fetchLocations"; + + // List of all module Measures Channel ids + public static final String CHANNEL_MEASURES_TEMPERATURE = "measures#temperature"; + public static final String CHANNEL_MEASURES_HUMIDITY = "measures#humidity"; + // List of all module Status Channel ids + public static final String CHANNEL_STATUS_STATE = "status#state"; + public static final String CHANNEL_STATUS_FUNCTION = "status#function"; + public static final String CHANNEL_STATUS_MODE = "status#mode"; + public static final String CHANNEL_STATUS_TEMPERATURE = "status#temperature"; + public static final String CHANNEL_STATUS_PROGRAM = "status#program"; + public static final String CHANNEL_STATUS_ENDTIME = "status#endTime"; + public static final String CHANNEL_STATUS_TEMP_FORMAT = "status#temperatureFormat"; + // List of all module Settings Channel ids + public static final String CHANNEL_SETTINGS_MODE = "settings#mode"; + public static final String CHANNEL_SETTINGS_TEMPERATURE = "settings#temperature"; + public static final String CHANNEL_SETTINGS_PROGRAM = "settings#program"; + public static final String CHANNEL_SETTINGS_BOOSTTIME = "settings#boostTime"; + public static final String CHANNEL_SETTINGS_ENDDATE = "settings#endDate"; + public static final String CHANNEL_SETTINGS_ENDHOUR = "settings#endHour"; + public static final String CHANNEL_SETTINGS_ENDMINUTE = "settings#endMinute"; + public static final String CHANNEL_SETTINGS_POWER = "settings#power"; + // List of all module Config Channel ids + public static final String CHANNEL_CONFIG_FETCH_PROGRAMS = "config#fetchPrograms"; + + // List of all adressable things + public static final Set SUPPORTED_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_BRIDGE, THING_TYPE_MODULE).collect(Collectors.toSet())); + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherAccountHandler.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherAccountHandler.java new file mode 100644 index 0000000000000..610f18be2891d --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherAccountHandler.java @@ -0,0 +1,224 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.account; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.openhab.binding.bticinosmarther.internal.api.dto.Location; +import org.openhab.binding.bticinosmarther.internal.api.dto.Module; +import org.openhab.binding.bticinosmarther.internal.api.dto.ModuleStatus; +import org.openhab.binding.bticinosmarther.internal.api.dto.Plant; +import org.openhab.binding.bticinosmarther.internal.api.dto.Program; +import org.openhab.binding.bticinosmarther.internal.api.dto.Subscription; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException; +import org.openhab.binding.bticinosmarther.internal.model.ModuleSettings; + +/** + * The {@code SmartherAccountHandler} interface is used to decouple the Smarther account handler implementation from + * other Bridge code. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public interface SmartherAccountHandler extends ThingHandler { + + /** + * Returns the {@link ThingUID} associated with this Smarther account handler. + * + * @return the thing UID associated with this Smarther account handler + */ + ThingUID getUID(); + + /** + * Returns the label of the Smarther Bridge associated with this Smarther account handler. + * + * @return a string containing the bridge label associated with the account handler + */ + String getLabel(); + + /** + * Returns the available locations associated with this Smarther account handler. + * + * @return the list of available locations, or an empty {@link List} in case of no locations found + */ + List getLocations(); + + /** + * Checks whether the given location is managed by this Smarther account handler + * + * @param plantId + * the identifier of the location to search for + * + * @return {@code true} if the given location is found, {@code false} otherwise + */ + boolean hasLocation(String plantId); + + /** + * Returns the plants registered under the Smarther account the bridge has been configured with. + * + * @return the list of registered plants, or an empty {@link List} in case of no plants found + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the Smarther API + */ + List getPlants() throws SmartherGatewayException; + + /** + * Returns the subscriptions registered to the C2C Webhook, where modules status notifications are currently sent + * for all the plants. + * + * @return the list of registered subscriptions, or an empty {@link List} in case of no subscriptions found + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the Smarther API + */ + List getSubscriptions() throws SmartherGatewayException; + + /** + * Subscribes a plant to the C2C Webhook to start receiving modules status notifications. + * + * @param plantId + * the identifier of the plant to be subscribed + * @param notificationUrl + * the url notifications will have to be sent to for the given plant + * + * @return the identifier this subscription has been registered under + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the Smarther API + */ + String subscribePlant(String plantId, String notificationUrl) throws SmartherGatewayException; + + /** + * Unsubscribes a plant from the C2C Webhook to stop receiving modules status notifications. + * + * @param plantId + * the identifier of the plant to be unsubscribed + * @param subscriptionId + * the identifier of the subscription to be removed for the given plant + * + * @return {@code true} if the plant is successfully unsubscribed, {@code false} otherwise + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the Smarther API + */ + void unsubscribePlant(String plantId, String subscriptionId) throws SmartherGatewayException; + + /** + * Returns the chronothermostat modules registered at the given location. + * + * @param location + * the identifier of the location + * + * @return the list of registered modules, or an empty {@link List} if the location contains no module or in case of + * communication issues with the Smarther API + */ + List getLocationModules(Location location); + + /** + * Returns the current status of a given chronothermostat module. + * + * @param plantId + * the identifier of the plant + * @param moduleId + * the identifier of the chronothermostat module inside the plant + * + * @return the current status of the chronothermostat module + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the Smarther API + */ + ModuleStatus getModuleStatus(String plantId, String moduleId) throws SmartherGatewayException; + + /** + * Sends new settings to be applied to a given chronothermostat module. + * + * @param settings + * the module settings to be applied + * + * @return {@code true} if the settings have been successfully applied, {@code false} otherwise + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the Smarther API + */ + boolean setModuleStatus(ModuleSettings moduleSettings) throws SmartherGatewayException; + + /** + * Returns the automatic mode programs registered for the given chronothermostat module. + * + * @param plantId + * the identifier of the plant + * @param moduleId + * the identifier of the chronothermostat module inside the plant + * + * @return the list of registered programs, or an empty {@link List} in case of no programs found + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the Smarther API + */ + List getModulePrograms(String plantId, String moduleId) throws SmartherGatewayException; + + /** + * Checks whether the Smarther Bridge associated with this Smarther account handler is authorized by Smarther API. + * + * @return {@code true} if the Bridge is authorized, {@code false} otherwise + */ + boolean isAuthorized(); + + /** + * Checks whether the Smarther Bridge thing is online. + * + * @return {@code true} if the Bridge is online, {@code false} otherwise + */ + boolean isOnline(); + + /** + * Performs the authorization procedure with Legrand/Bticino portal. + * In case of success, the returned refresh/access tokens and the notification url are stored in the Bridge. + * + * @param redirectUrl + * the redirect url BTicino/Legrand portal calls back to + * @param reqCode + * the unique code passed by BTicino/Legrand portal to obtain the refresh and access tokens + * @param notificationUrl + * the endpoint C2C Webhook service will send module status notifications to, once authorized + * + * @return a string containing the name of the BTicino/Legrand portal user that is authorized + */ + String authorize(String redirectUrl, String reqCode, String notificationUrl) throws SmartherGatewayException; + + /** + * Compares this Smarther account handler instance to a given Thing UID. + * + * @param thingUID + * the Thing UID the account handler is compared to + * + * @return {@code true} if the two instances match, {@code false} otherwise + */ + boolean equalsThingUID(String thingUID); + + /** + * Formats the url used to call the Smarther API in order to authorize the Smarther Bridge associated with this + * Smarther account handler. + * + * @param redirectUri + * the uri BTicino/Legrand portal redirects back to + * + * @return a string containing the formatted url, or the empty string ("") in case of issue + */ + String formatAuthorizationUrl(String redirectUri); + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherAccountService.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherAccountService.java new file mode 100644 index 0000000000000..d5fb334fc4889 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherAccountService.java @@ -0,0 +1,292 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.account; + +import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Hashtable; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.util.ConcurrentHashSet; +import org.openhab.binding.bticinosmarther.internal.api.dto.Notification; +import org.openhab.binding.bticinosmarther.internal.api.dto.Sender; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException; +import org.openhab.binding.bticinosmarther.internal.util.StringUtil; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.http.HttpService; +import org.osgi.service.http.NamespaceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@code SmartherAccountService} class manages the servlets and bind authorization servlet to Bridges. + * + * @author Fabio Possieri - Initial contribution + */ +@Component(service = SmartherAccountService.class, immediate = true, configurationPid = "binding.bticinosmarther.accountService") +@NonNullByDefault +public class SmartherAccountService { + + private static final String TEMPLATE_PATH = "templates/"; + private static final String IMAGE_PATH = "web"; + private static final String TEMPLATE_APPLICATION = TEMPLATE_PATH + "application.html"; + private static final String TEMPLATE_INDEX = TEMPLATE_PATH + "index.html"; + private static final String ERROR_UKNOWN_BRIDGE = "Returned 'state' doesn't match any Bridges. Has the bridge been removed?"; + + private final Logger logger = LoggerFactory.getLogger(SmartherAccountService.class); + + private final Set handlers = new ConcurrentHashSet<>(); + + private @Nullable HttpService httpService; + private @Nullable BundleContext bundleContext; + + @Activate + protected void activate(ComponentContext componentContext, Map properties) { + try { + this.bundleContext = componentContext.getBundleContext(); + + final HttpService localHttpService = this.httpService; + if (localHttpService != null) { + // Register the authorization servlet + localHttpService.registerServlet(AUTH_SERVLET_ALIAS, createAuthorizationServlet(), new Hashtable<>(), + localHttpService.createDefaultHttpContext()); + localHttpService.registerResources(AUTH_SERVLET_ALIAS + IMG_SERVLET_ALIAS, IMAGE_PATH, null); + + // Register the notification servlet + localHttpService.registerServlet(NOTIFY_SERVLET_ALIAS, createNotificationServlet(), new Hashtable<>(), + localHttpService.createDefaultHttpContext()); + } + } catch (NamespaceException | ServletException | IOException e) { + logger.warn("Error during Smarther servlet startup", e); + } + } + + @Deactivate + protected void deactivate(ComponentContext componentContext) { + final HttpService localHttpService = this.httpService; + if (localHttpService != null) { + // Unregister the authorization servlet + localHttpService.unregister(AUTH_SERVLET_ALIAS); + localHttpService.unregister(AUTH_SERVLET_ALIAS + IMG_SERVLET_ALIAS); + + // Unregister the notification servlet + localHttpService.unregister(NOTIFY_SERVLET_ALIAS); + } + } + + /** + * Constructs a {@code SmartherAuthorizationServlet}. + * + * @return the newly created servlet + * + * @throws {@link IOException} + * in case of issues reading one of the internal html templates + */ + private HttpServlet createAuthorizationServlet() throws IOException { + return new SmartherAuthorizationServlet(this, readTemplate(TEMPLATE_INDEX), readTemplate(TEMPLATE_APPLICATION)); + } + + /** + * Constructs a {@code SmartherNotificationServlet}. + * + * @return the newly created servlet + */ + private HttpServlet createNotificationServlet() { + return new SmartherNotificationServlet(this); + } + + /** + * Reads a template from file and returns its content as string. + * + * @param templateName + * the name of the template file to read + * + * @return a string representing the content of the template file + * + * @throws {@link IOException} + * in case of issues reading the template from file + */ + private String readTemplate(String templateName) throws IOException { + final BundleContext localBundleContext = this.bundleContext; + if (localBundleContext != null) { + final URL index = localBundleContext.getBundle().getEntry(templateName); + + if (index == null) { + throw new FileNotFoundException(String + .format("Cannot find template '%s' - failed to initialize Smarther servlet", templateName)); + } else { + try (InputStream input = index.openStream()) { + return StringUtil.streamToString(input); + } + } + } else { + throw new IOException("Cannot get template, bundle context is null"); + } + } + + /** + * Dispatches the received Smarther API authorization response to the proper Smarther account handler. + * Part of the Legrand/Bticino OAuth2 authorization process. + * + * @param servletBaseURL + * the authorization servlet url needed to derive the notification endpoint url + * @param state + * the authorization state needed to match the correct Smarther account handler to authorize + * @param code The BTicino/Legrand API returned code value + * the authorization code to authorize with the account handler + * + * @return a string containing the name of the authorized BTicino/Legrand portal user + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the Smarther API or no account handler found + */ + public String dispatchAuthorization(String servletBaseURL, String state, String code) + throws SmartherGatewayException { + // Searches the SmartherAccountHandler instance that matches the given state + final SmartherAccountHandler accountHandler = getAccountHandlerByUID(state); + if (accountHandler != null) { + // Generates the notification URL from servletBaseURL + final String notificationUrl = servletBaseURL.replace(AUTH_SERVLET_ALIAS, NOTIFY_SERVLET_ALIAS); + + logger.debug("API authorization: calling authorize on {}", accountHandler.getUID()); + + // Passes the authorization to the handler + return accountHandler.authorize(servletBaseURL, code, notificationUrl); + } else { + logger.trace("API authorization: request redirected with state '{}'", state); + logger.warn("API authorization: no matching bridge was found. Possible bridge has been removed."); + throw new SmartherGatewayException(ERROR_UKNOWN_BRIDGE); + } + } + + /** + * Dispatches the received C2C Webhook notification to the proper Smarther notification handler. + * + * @param notification + * the received notification to handle + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the Smarther API or no notification handler found + */ + public void dispatchNotification(Notification notification) throws SmartherGatewayException { + final Sender sender = notification.getSender(); + if (sender != null) { + // Searches the SmartherAccountHandler instance that matches the given location + final SmartherAccountHandler accountHandler = getAccountHandlerByLocation(sender.getPlant().getId()); + if (accountHandler == null) { + logger.warn("C2C notification [{}]: no matching bridge was found. Possible bridge has been removed.", + notification.getId()); + throw new SmartherGatewayException(ERROR_UKNOWN_BRIDGE); + } else if (accountHandler.isOnline()) { + final SmartherNotificationHandler notificationHandler = (SmartherNotificationHandler) accountHandler; + + if (notificationHandler.useNotifications()) { + // Passes the notification to the handler + notificationHandler.handleNotification(notification); + } else { + logger.debug( + "C2C notification [{}]: notification discarded as bridge does not handle notifications.", + notification.getId()); + } + } else { + logger.debug("C2C notification [{}]: notification discarded as bridge is offline.", + notification.getId()); + } + } else { + logger.debug("C2C notification [{}]: notification discarded as payload is invalid.", notification.getId()); + } + } + + /** + * Adds a {@link SmartherAccountHandler} handler to the set of account service handlers. + * + * @param handler + * the handler to add to the handlers set + */ + public void addSmartherAccountHandler(SmartherAccountHandler handler) { + handlers.add(handler); + } + + /** + * Removes a {@link SmartherAccountHandler} handler from the set of account service handlers. + * + * @param handler + * the handler to remove from the handlers set + */ + public void removeSmartherAccountHandler(SmartherAccountHandler handler) { + handlers.remove(handler); + } + + /** + * Returns all the {@link SmartherAccountHandler} account service handlers. + * + * @return a set containing all the account service handlers + */ + public Set getSmartherAccountHandlers() { + return handlers; + } + + /** + * Searches the {@link SmartherAccountHandler} handler that matches the given Thing UID. + * + * @param thingUID + * the UID of the Thing to match the handler with + * + * @return the handler matching the given Thing UID, or {@code null} if none matches + */ + private @Nullable SmartherAccountHandler getAccountHandlerByUID(String thingUID) { + final Optional maybeHandler = handlers.stream().filter(l -> l.equalsThingUID(thingUID)) + .findFirst(); + return (maybeHandler.isPresent()) ? maybeHandler.get() : null; + } + + /** + * Searches the {@link SmartherAccountHandler} handler that matches the given location plant. + * + * @param plantId + * the identifier of the plant to match the handler with + * + * @return the handler matching the given location plant, or {@code null} if none matches + */ + private @Nullable SmartherAccountHandler getAccountHandlerByLocation(String plantId) { + final Optional maybeHandler = handlers.stream().filter(l -> l.hasLocation(plantId)) + .findFirst(); + return (maybeHandler.isPresent()) ? maybeHandler.get() : null; + } + + @Reference + protected void setHttpService(HttpService httpService) { + this.httpService = httpService; + } + + protected void unsetHttpService(HttpService httpService) { + this.httpService = null; + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherAuthorizationServlet.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherAuthorizationServlet.java new file mode 100644 index 0000000000000..5943c2ef230d3 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherAuthorizationServlet.java @@ -0,0 +1,272 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.account; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.UrlEncoded; +import org.openhab.binding.bticinosmarther.internal.api.dto.Location; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException; +import org.openhab.binding.bticinosmarther.internal.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@code SmartherAuthorizationServlet} class acts as the registered endpoint for the user to automatically manage + * the BTicino/Legrand API authorization process. + * The servlet follows the OAuth2 Authorization Code flow, saving the resulting refreshToken within the Smarther Bridge. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherAuthorizationServlet extends HttpServlet { + + private static final long serialVersionUID = 5199173744807168342L; + + private static final String CONTENT_TYPE = "text/html;charset=UTF-8"; + private static final String X_FORWARDED_PROTO = "X-Forwarded-Proto"; + + // Http request parameters + private static final String PARAM_CODE = "code"; + private static final String PARAM_STATE = "state"; + private static final String PARAM_ERROR = "error"; + + // Simple HTML templates for inserting messages. + private static final String HTML_EMPTY_APPLICATIONS = "

Manually add a Smarther Bridge to authorize it here

"; + private static final String HTML_BRIDGE_AUTHORIZED = "

Bridge authorized for Client Id %s

"; + private static final String HTML_ERROR = "

Call to Smarther API gateway failed with error: %s

"; + + private static final Pattern MESSAGE_KEY_PATTERN = Pattern.compile("\\$\\{([^\\}]+)\\}"); + + // Keys present in the index.html + private static final String KEY_PAGE_REFRESH = "pageRefresh"; + private static final String HTML_META_REFRESH_CONTENT = ""; + private static final String KEY_AUTHORIZED_BRIDGE = "authorizedBridge"; + private static final String KEY_ERROR = "error"; + private static final String KEY_APPLICATIONS = "applications"; + private static final String KEY_REDIRECT_URI = "redirectUri"; + // Keys present in the application.html + private static final String APPLICATION_ID = "application.id"; + private static final String APPLICATION_NAME = "application.name"; + private static final String APPLICATION_LOCATIONS = "application.locations"; + private static final String APPLICATION_AUTHORIZED_CLASS = "application.authorized"; + private static final String APPLICATION_AUTHORIZE = "application.authorize"; + + private final Logger logger = LoggerFactory.getLogger(SmartherAuthorizationServlet.class); + + private final SmartherAccountService accountService; + private final String indexTemplate; + private final String applicationTemplate; + + /** + * Constructs a {@code SmartherAuthorizationServlet} associated to the given {@link SmartherAccountService} service + * and with the given html index/application templates. + * + * @param accountService + * the account service to associate to the servlet + * @param indexTemplate + * the html template to use as index page for the user + * @param applicationTemplate + * the html template to use as application page for the user + */ + public SmartherAuthorizationServlet(SmartherAccountService accountService, String indexTemplate, + String applicationTemplate) { + this.accountService = accountService; + this.indexTemplate = indexTemplate; + this.applicationTemplate = applicationTemplate; + } + + @Override + protected void doGet(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response) + throws ServletException, IOException { + if (request != null && response != null) { + final String servletBaseURL = extractServletBaseURL(request); + logger.debug("Authorization callback servlet received GET request {}", servletBaseURL); + + // Handle the received data + final Map replaceMap = new HashMap<>(); + handleSmartherRedirect(replaceMap, servletBaseURL, request.getQueryString()); + + // Build a http 200 (Success) response for the caller + response.setContentType(CONTENT_TYPE); + response.setStatus(HttpStatus.OK_200); + replaceMap.put(KEY_REDIRECT_URI, servletBaseURL); + replaceMap.put(KEY_APPLICATIONS, formatApplications(applicationTemplate, servletBaseURL)); + response.getWriter().append(replaceKeysFromMap(indexTemplate, replaceMap)); + response.getWriter().close(); + } else if (response != null) { + // Build a http 400 (Bad Request) error response for the caller + response.setContentType(CONTENT_TYPE); + response.setStatus(HttpStatus.BAD_REQUEST_400); + response.getWriter().close(); + } else { + throw new ServletException("Authorization callback with null request/response"); + } + } + + /** + * Extracts the servlet base url from the received http request, handling eventual reverse proxy. + * + * @param request + * the received http request + * + * @return a string containing the servlet base url + */ + private String extractServletBaseURL(HttpServletRequest request) { + final StringBuffer requestURL = request.getRequestURL(); + + // Try to infer the real protocol from request headers + final String realProtocol = StringUtil.defaultIfBlank(request.getHeader(X_FORWARDED_PROTO), + request.getScheme()); + + return requestURL.replace(0, requestURL.indexOf(":"), realProtocol).toString(); + } + + /** + * Handles a call from BTicino/Legrand API gateway to the redirect_uri, dispatching the authorization flow to the + * proper authorization handler. + * If the user was authorized, this is passed on to the handler; in case of an error, this is shown to the user. + * Based on all these different outcomes the html response is generated to inform the user. + * + * @param replaceMap + * a map with key string values to use in the html templates + * @param servletBaseURL + * the servlet base url to compose the correct API gateway redirect_uri + * @param queryString + * the querystring part of the received request, may be {@code null} + */ + private void handleSmartherRedirect(Map replaceMap, String servletBaseURL, + @Nullable String queryString) { + replaceMap.put(KEY_AUTHORIZED_BRIDGE, ""); + replaceMap.put(KEY_ERROR, ""); + replaceMap.put(KEY_PAGE_REFRESH, ""); + + if (queryString != null) { + final MultiMap params = new MultiMap<>(); + UrlEncoded.decodeTo(queryString, params, StandardCharsets.UTF_8.name()); + final String reqCode = params.getString(PARAM_CODE); + final String reqState = params.getString(PARAM_STATE); + final String reqError = params.getString(PARAM_ERROR); + + replaceMap.put(KEY_PAGE_REFRESH, + params.isEmpty() ? "" : String.format(HTML_META_REFRESH_CONTENT, servletBaseURL)); + if (!StringUtil.isBlank(reqError)) { + logger.debug("Authorization redirected with an error: {}", reqError); + replaceMap.put(KEY_ERROR, String.format(HTML_ERROR, reqError)); + } else if (!StringUtil.isBlank(reqState)) { + try { + logger.trace("Received from authorization - state:[{}] code:[{}]", reqState, reqCode); + replaceMap.put(KEY_AUTHORIZED_BRIDGE, String.format(HTML_BRIDGE_AUTHORIZED, + accountService.dispatchAuthorization(servletBaseURL, reqState, reqCode))); + } catch (SmartherGatewayException e) { + logger.debug("Exception during authorizaton: ", e); + replaceMap.put(KEY_ERROR, String.format(HTML_ERROR, e.getMessage())); + } + } + } + } + + /** + * Returns an html formatted text representing all the available Smarther Bridge applications. + * + * @param applicationTemplate + * the html template to format the application with + * @param servletBaseURL + * the redirect_uri to link to the authorization button as authorization url + * + * @return a string containing the html formatted text + */ + private String formatApplications(String applicationTemplate, String servletBaseURL) { + final Set applications = accountService.getSmartherAccountHandlers(); + + return applications.isEmpty() ? HTML_EMPTY_APPLICATIONS + : applications.stream().map(p -> formatApplication(applicationTemplate, p, servletBaseURL)) + .collect(Collectors.joining()); + } + + /** + * Returns an html formatted text representing a given Smarther Bridge application. + * + * @param applicationTemplate + * the html template to format the application with + * @param handler + * the Smarther application handler to use + * @param servletBaseURL + * the redirect_uri to link to the authorization button as authorization url + * + * @return a string containing the html formatted text + */ + private String formatApplication(String applicationTemplate, SmartherAccountHandler handler, + String servletBaseURL) { + final Map map = new HashMap<>(); + + map.put(APPLICATION_ID, handler.getUID().getAsString()); + map.put(APPLICATION_NAME, handler.getLabel()); + + if (handler.isAuthorized()) { + final String availableLocations = Location.toNameString(handler.getLocations()); + map.put(APPLICATION_AUTHORIZED_CLASS, " authorized"); + map.put(APPLICATION_LOCATIONS, String.format(" (Available locations: %s)", availableLocations)); + } else { + map.put(APPLICATION_AUTHORIZED_CLASS, ""); + map.put(APPLICATION_LOCATIONS, ""); + } + + map.put(APPLICATION_AUTHORIZE, handler.formatAuthorizationUrl(servletBaseURL)); + return replaceKeysFromMap(applicationTemplate, map); + } + + /** + * Replaces all keys found in the template with the values matched from the map. + * If a key is not found in the map, it is kept unchanged in the template. + * + * @param template + * the template to replace keys on + * @param map + * the map containing the key/value pairs to replace in the template + * + * @return a string containing the resulting template after the replace process + */ + private String replaceKeysFromMap(String template, Map map) { + final Matcher m = MESSAGE_KEY_PATTERN.matcher(template); + final StringBuffer sb = new StringBuffer(); + + while (m.find()) { + try { + final String key = m.group(1); + m.appendReplacement(sb, Matcher.quoteReplacement(map.getOrDefault(key, "${" + key + '}'))); + } catch (RuntimeException e) { + logger.warn("Error occurred during template filling, cause ", e); + } + } + m.appendTail(sb); + return sb.toString(); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherNotificationHandler.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherNotificationHandler.java new file mode 100644 index 0000000000000..b019027a79cb1 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherNotificationHandler.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.account; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.openhab.binding.bticinosmarther.internal.api.dto.Notification; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException; + +/** + * The {@code SmartherNotificationHandler} interface is used to decouple the Smarther notification handler + * implementation from other Bridge code. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public interface SmartherNotificationHandler extends ThingHandler { + + /** + * Tells whether the Smarther Bridge associated with this handler supports notifications. + * + * @return {@code true} if the Bridge supports notifications, {@code false} otherwise + */ + boolean useNotifications(); + + /** + * Calls the Smarther API to register a new notification endpoint to the C2C Webhook service. + * + * @param plantId + * the identifier of the plant the notification endpoint belongs to + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the Smarther API + */ + void registerNotification(String plantId) throws SmartherGatewayException; + + /** + * Handles a new notifications received from the C2C Webhook notification service. + * + * @param notification + * the received notification + */ + void handleNotification(Notification notification); + + /** + * Calls the Smarther API to unregister a notification endpoint already registered to the C2C Webhook service. + * + * @param plantId + * the identifier of the plant the notification endpoint belongs to + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the Smarther API + */ + void unregisterNotification(String plantId) throws SmartherGatewayException; + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherNotificationServlet.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherNotificationServlet.java new file mode 100644 index 0000000000000..b36916dc3f1b0 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/account/SmartherNotificationServlet.java @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.account; + +import java.io.IOException; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.bticinosmarther.internal.api.dto.Notification; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException; +import org.openhab.binding.bticinosmarther.internal.util.ModelUtil; +import org.openhab.binding.bticinosmarther.internal.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; + +/** + * The {@code SmartherNotificationServlet} class acts as the registered endpoint to receive module status notifications + * from the Legrand/Bticino C2C Webhook notification service. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherNotificationServlet extends HttpServlet { + + private static final long serialVersionUID = -2474355132186048438L; + + private static final String CONTENT_TYPE = "application/json;charset=UTF-8"; + private static final String OK_RESULT_MSG = "{\"result\":0}"; + private static final String KO_RESULT_MSG = "{\"result\":1}"; + + private final Logger logger = LoggerFactory.getLogger(SmartherNotificationServlet.class); + + private final SmartherAccountService accountService; + + /** + * Constructs a {@code SmartherNotificationServlet} associated to the given {@link SmartherAccountService} service. + * + * @param accountService + * the account service to associate to the servlet + */ + public SmartherNotificationServlet(SmartherAccountService accountService) { + this.accountService = accountService; + } + + @Override + protected void doPost(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response) + throws ServletException, IOException { + if (request != null && response != null) { + logger.debug("Notification callback servlet received POST request {}", request.getRequestURI()); + + // Handle the received data + final String requestBody = StringUtil.readerToString(request.getReader()); + final String responseBody = dispatchNotifications(requestBody); + + // Build a http 200 (Success) response for the caller + response.setContentType(CONTENT_TYPE); + response.setStatus(HttpStatus.OK_200); + response.getWriter().append(responseBody); + response.getWriter().close(); + } else if (response != null) { + // Build a http 400 (Bad Request) error response for the caller + response.setContentType(CONTENT_TYPE); + response.setStatus(HttpStatus.BAD_REQUEST_400); + response.getWriter().close(); + } else { + throw new ServletException("Notification callback with null request/response"); + } + } + + /** + * Dispatches all the notifications contained in the received payload to the proper notification handlers. + * The response to the notification service is generated based on the different outcomes. + * + * @param payload + * the received servlet payload to process, may be {@code null} + * + * @return a string containing the response to the notification service + */ + private String dispatchNotifications(@Nullable String payload) { + try { + logger.trace("C2C listener received payload: {}", payload); + if (!StringUtil.isBlank(payload)) { + List notifications = ModelUtil.gsonInstance().fromJson(payload, + new TypeToken>() { + }.getType()); + + if (notifications != null) { + notifications.forEach(n -> handleSmartherNotification(n)); + } + } + return OK_RESULT_MSG; + } catch (JsonSyntaxException e) { + logger.warn("C2C payload parsing error: {} ", e.getMessage()); + return KO_RESULT_MSG; + } + } + + /** + * Dispatches a single notification contained in the received payload to the proper notification handler. + * + * @param notification + * the notification to dispatch + */ + private void handleSmartherNotification(Notification notification) { + try { + this.accountService.dispatchNotification(notification); + } catch (SmartherGatewayException e) { + logger.warn("C2C notification {}: not applied: {}", notification.getId(), e.getMessage()); + } + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/SmartherApi.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/SmartherApi.java new file mode 100644 index 0000000000000..44a49755fb2e3 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/SmartherApi.java @@ -0,0 +1,505 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api; + +import static org.eclipse.jetty.http.HttpMethod.*; +import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Function; + +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.smarthome.core.auth.client.oauth2.AccessTokenResponse; +import org.eclipse.smarthome.core.auth.client.oauth2.OAuthClientService; +import org.eclipse.smarthome.core.auth.client.oauth2.OAuthException; +import org.eclipse.smarthome.core.auth.client.oauth2.OAuthResponseException; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.openhab.binding.bticinosmarther.internal.api.dto.Chronothermostat; +import org.openhab.binding.bticinosmarther.internal.api.dto.Module; +import org.openhab.binding.bticinosmarther.internal.api.dto.ModuleStatus; +import org.openhab.binding.bticinosmarther.internal.api.dto.Plant; +import org.openhab.binding.bticinosmarther.internal.api.dto.Plants; +import org.openhab.binding.bticinosmarther.internal.api.dto.Program; +import org.openhab.binding.bticinosmarther.internal.api.dto.Subscription; +import org.openhab.binding.bticinosmarther.internal.api.dto.Topology; +import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.MeasureUnit; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherAuthorizationException; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherTokenExpiredException; +import org.openhab.binding.bticinosmarther.internal.model.ModuleSettings; +import org.openhab.binding.bticinosmarther.internal.util.ModelUtil; +import org.openhab.binding.bticinosmarther.internal.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; + +/** + * The {@code SmartherApi} class is used to communicate with the BTicino/Legrand API gateway. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherApi { + + private static final String CONTENT_TYPE = "application/json"; + private static final String BEARER = "Bearer "; + + // API gateway request headers + private static final String HEADER_ACCEPT = "Accept"; + // API gateway request attributes + private static final String ATTR_FUNCTION = "function"; + private static final String ATTR_MODE = "mode"; + private static final String ATTR_PROGRAMS = "programs"; + private static final String ATTR_NUMBER = "number"; + private static final String ATTR_SETPOINT = "setPoint"; + private static final String ATTR_VALUE = "value"; + private static final String ATTR_UNIT = "unit"; + private static final String ATTR_ACTIVATION_TIME = "activationTime"; + private static final String ATTR_ENDPOINT_URL = "EndPointUrl"; + // API gateway operation paths + private static final String PATH_PLANTS = "/plants"; + private static final String PATH_TOPOLOGY = PATH_PLANTS + "/%s/topology"; + private static final String PATH_MODULE = "/chronothermostat/thermoregulation/addressLocation/plants/%s/modules/parameter/id/value/%s"; + private static final String PATH_PROGRAMS = "/programlist"; + private static final String PATH_SUBSCRIPTIONS = "/subscription"; + private static final String PATH_SUBSCRIBE = PATH_PLANTS + "/%s/subscription"; + private static final String PATH_UNSUBSCRIBE = PATH_SUBSCRIBE + "/%s"; + + private final Logger logger = LoggerFactory.getLogger(SmartherApi.class); + + private final OAuthClientService oAuthClientService; + private final String oAuthSubscriptionKey; + private final SmartherApiConnector connector; + + /** + * Constructs a {@code SmartherApi} to the API gateway with the specified OAuth2 attributes (subscription key and + * client service), scheduler service and http client. + * + * @param clientService + * the OAuth2 authorization client service to be used + * @param subscriptionKey + * the OAuth2 subscription key to be used with the given client service + * @param scheduler + * the scheduler to be used to reschedule calls when rate limit exceeded or call not succeeded + * @param httpClient + * the http client to be used to make http calls to the API gateway + */ + public SmartherApi(final OAuthClientService clientService, final String subscriptionKey, + final ScheduledExecutorService scheduler, final HttpClient httpClient) { + this.oAuthClientService = clientService; + this.oAuthSubscriptionKey = subscriptionKey; + this.connector = new SmartherApiConnector(scheduler, httpClient); + } + + /** + * Returns the plants registered under the Smarther account the bridge has been configured with. + * + * @return the list of registered plants, or an empty {@link List} in case of no plants found + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the API gateway + */ + public List getPlants() throws SmartherGatewayException { + try { + final ContentResponse response = requestBasic(GET, PATH_PLANTS); + if (response.getStatus() == HttpStatus.NO_CONTENT_204) { + return new ArrayList<>(); + } else { + return ModelUtil.gsonInstance().fromJson(response.getContentAsString(), Plants.class).getPlants(); + } + } catch (JsonSyntaxException e) { + throw new SmartherGatewayException(e.getMessage()); + } + } + + /** + * Returns the chronothermostat modules registered in the given plant. + * + * @param plantId + * the identifier of the plant + * + * @return the list of registered modules, or an empty {@link List} in case the plant contains no module + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the API gateway + */ + public List getPlantModules(String plantId) throws SmartherGatewayException { + try { + final ContentResponse response = requestBasic(GET, String.format(PATH_TOPOLOGY, plantId)); + final Topology topology = ModelUtil.gsonInstance().fromJson(response.getContentAsString(), Topology.class); + return topology.getModules(); + } catch (JsonSyntaxException e) { + throw new SmartherGatewayException(e.getMessage()); + } + } + + /** + * Returns the current status of a given chronothermostat module. + * + * @param plantId + * the identifier of the plant + * @param moduleId + * the identifier of the chronothermostat module inside the plant + * + * @return the current status of the chronothermostat module + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the API gateway + */ + public ModuleStatus getModuleStatus(String plantId, String moduleId) throws SmartherGatewayException { + try { + final ContentResponse response = requestModule(GET, plantId, moduleId, null); + return ModelUtil.gsonInstance().fromJson(response.getContentAsString(), ModuleStatus.class); + } catch (JsonSyntaxException e) { + throw new SmartherGatewayException(e.getMessage()); + } + } + + /** + * Sends new settings to be applied to a given chronothermostat module. + * + * @param settings + * the module settings to be applied + * + * @return {@code true} if the settings have been successfully applied, {@code false} otherwise + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the API gateway + */ + public boolean setModuleStatus(ModuleSettings settings) throws SmartherGatewayException { + // Prepare request payload + Map rootMap = new IdentityHashMap<>(); + rootMap.put(ATTR_FUNCTION, settings.getFunction().getValue()); + rootMap.put(ATTR_MODE, settings.getMode().getValue()); + switch (settings.getMode()) { + case AUTOMATIC: + // {"function":"heating","mode":"automatic","programs":[{"number":0}]} + Map programMap = new IdentityHashMap(); + programMap.put(ATTR_NUMBER, Integer.valueOf(settings.getProgram())); + List> programsList = new ArrayList<>(); + programsList.add(programMap); + rootMap.put(ATTR_PROGRAMS, programsList); + break; + case MANUAL: + // {"function":"heating","mode":"manual","setPoint":{"value":0.0,"unit":"C"},"activationTime":"X"} + QuantityType newTemperature = settings.getSetPointTemperature(SIUnits.CELSIUS); + if (newTemperature == null) { + throw new SmartherGatewayException("Invalid temperature unit transformation"); + } + Map setPointMap = new IdentityHashMap(); + setPointMap.put(ATTR_VALUE, newTemperature.doubleValue()); + setPointMap.put(ATTR_UNIT, MeasureUnit.CELSIUS.getValue()); + rootMap.put(ATTR_SETPOINT, setPointMap); + rootMap.put(ATTR_ACTIVATION_TIME, settings.getActivationTime()); + break; + case BOOST: + // {"function":"heating","mode":"boost","activationTime":"X"} + rootMap.put(ATTR_ACTIVATION_TIME, settings.getActivationTime()); + break; + case OFF: + // {"function":"heating","mode":"off"} + break; + case PROTECTION: + // {"function":"heating","mode":"protection"} + break; + } + final String jsonPayload = ModelUtil.gsonInstance().toJson(rootMap); + + // Send request to server + final ContentResponse response = requestModule(POST, settings.getPlantId(), settings.getModuleId(), + jsonPayload); + return (response.getStatus() == HttpStatus.OK_200); + } + + /** + * Returns the automatic mode programs registered for the given chronothermostat module. + * + * @param plantId + * the identifier of the plant + * @param moduleId + * the identifier of the chronothermostat module inside the plant + * + * @return the list of registered programs, or an empty {@link List} in case of no programs found + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the API gateway + */ + public List getModulePrograms(String plantId, String moduleId) throws SmartherGatewayException { + try { + final ContentResponse response = requestModule(GET, plantId, moduleId, PATH_PROGRAMS, null); + final ModuleStatus moduleStatus = ModelUtil.gsonInstance().fromJson(response.getContentAsString(), + ModuleStatus.class); + + final Chronothermostat chronothermostat = moduleStatus.toChronothermostat(); + return (chronothermostat != null) ? chronothermostat.getPrograms() : Collections.emptyList(); + } catch (JsonSyntaxException e) { + throw new SmartherGatewayException(e.getMessage()); + } + } + + /** + * Returns the subscriptions registered to the C2C Webhook, where modules status notifications are currently sent + * for all the plants. + * + * @return the list of registered subscriptions, or an empty {@link List} in case of no subscriptions found + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the API gateway + */ + public List getSubscriptions() throws SmartherGatewayException { + try { + final ContentResponse response = requestBasic(GET, PATH_SUBSCRIPTIONS); + if (response.getStatus() == HttpStatus.NO_CONTENT_204) { + return new ArrayList<>(); + } else { + return ModelUtil.gsonInstance().fromJson(response.getContentAsString(), + new TypeToken>() { + }.getType()); + } + } catch (JsonSyntaxException e) { + throw new SmartherGatewayException(e.getMessage()); + } + } + + /** + * Subscribes a plant to the C2C Webhook to start receiving modules status notifications. + * + * @param plantId + * the identifier of the plant to be subscribed + * @param notificationUrl + * the url notifications will have to be sent to for the given plant + * + * @return the identifier this subscription has been registered under + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the API gateway + */ + public String subscribePlant(String plantId, String notificationUrl) throws SmartherGatewayException { + try { + // Prepare request payload + Map rootMap = new IdentityHashMap(); + rootMap.put(ATTR_ENDPOINT_URL, notificationUrl); + final String jsonPayload = ModelUtil.gsonInstance().toJson(rootMap); + // Send request to server + final ContentResponse response = requestBasic(POST, String.format(PATH_SUBSCRIBE, plantId), jsonPayload); + // Handle response payload + final Subscription subscription = ModelUtil.gsonInstance().fromJson(response.getContentAsString(), + Subscription.class); + return subscription.getSubscriptionId(); + } catch (JsonSyntaxException e) { + throw new SmartherGatewayException(e.getMessage()); + } + } + + /** + * Unsubscribes a plant from the C2C Webhook to stop receiving modules status notifications. + * + * @param plantId + * the identifier of the plant to be unsubscribed + * @param subscriptionId + * the identifier of the subscription to be removed for the given plant + * + * @return {@code true} if the plant is successfully unsubscribed, {@code false} otherwise + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the API gateway + */ + public boolean unsubscribePlant(String plantId, String subscriptionId) throws SmartherGatewayException { + final ContentResponse response = requestBasic(DELETE, String.format(PATH_UNSUBSCRIBE, plantId, subscriptionId)); + return (response.getStatus() == HttpStatus.OK_200); + } + + // =========================================================================== + // + // Internal API call handling methods + // + // =========================================================================== + + /** + * Calls the API gateway with the given http method, request url and actual data. + * + * @param method + * the http method to make the call with + * @param url + * the API operation url to call + * @param requestData + * the actual data to send in the request body, may be {@code null} + * + * @return the response received from the API gateway + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the API gateway + */ + private ContentResponse requestBasic(HttpMethod method, String url, @Nullable String requestData) + throws SmartherGatewayException { + return request(method, SMARTHER_API_URL + url, requestData); + } + + /** + * Calls the API gateway with the given http method and request url. + * + * @param method + * the http method to make the call with + * @param url + * the API operation url to call + * + * @return the response received from the API gateway + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the API gateway + */ + private ContentResponse requestBasic(HttpMethod method, String url) throws SmartherGatewayException { + return requestBasic(method, url, null); + } + + /** + * Calls the API gateway with the given http method, plant id, module id, request path and actual data. + * + * @param method + * the http method to make the call with + * @param plantId + * the identifier of the plant to use + * @param moduleId + * the identifier of the module to use + * @param path + * the API operation relative path to call, may be {@code null} + * @param requestData + * the actual data to send in the request body, may be {@code null} + * + * @return the response received from the API gateway + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the API gateway + */ + private ContentResponse requestModule(HttpMethod method, String plantId, String moduleId, @Nullable String path, + @Nullable String requestData) throws SmartherGatewayException { + final String url = String.format(PATH_MODULE, plantId, moduleId) + StringUtil.defaultString(path); + return requestBasic(method, url, requestData); + } + + /** + * Calls the API gateway with the given http method, plant id, module id and actual data. + * + * @param method + * the http method to make the call with + * @param plantId + * the identifier of the plant to use + * @param moduleId + * the identifier of the module to use + * @param requestData + * the actual data to send in the request body, may be {@code null} + * + * @return the response received from the API gateway + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the API gateway + */ + private ContentResponse requestModule(HttpMethod method, String plantId, String moduleId, + @Nullable String requestData) throws SmartherGatewayException { + return requestModule(method, plantId, moduleId, null, requestData); + } + + /** + * Calls the API gateway with the given http method, request url and actual data. + * + * @param method + * the http method to make the call with + * @param url + * the API operation url to call + * @param requestData + * the actual data to send in the request body, may be {@code null} + * + * @return the response received from the API gateway + * + * @throws {@link SmartherGatewayException} + * in case of communication issues with the API gateway + */ + private ContentResponse request(HttpMethod method, String url, @Nullable String requestData) + throws SmartherGatewayException { + logger.debug("Request: ({}) {} - {}", method, url, StringUtil.defaultString(requestData)); + Function call = httpClient -> httpClient.newRequest(url).method(method) + .header(HEADER_ACCEPT, CONTENT_TYPE) + .content(new StringContentProvider(StringUtil.defaultString(requestData)), CONTENT_TYPE); + + try { + final AccessTokenResponse accessTokenResponse = oAuthClientService.getAccessTokenResponse(); + final String accessToken = (accessTokenResponse == null) ? null : accessTokenResponse.getAccessToken(); + + if (accessToken == null || accessToken.isEmpty()) { + throw new SmartherAuthorizationException(String + .format("No gateway accesstoken. Did you authorize smarther via %s ?", AUTH_SERVLET_ALIAS)); + } else { + return requestWithRetry(call, accessToken); + } + } catch (SmartherGatewayException e) { + throw e; + } catch (OAuthException | OAuthResponseException e) { + throw new SmartherAuthorizationException(e.getMessage(), e); + } catch (IOException e) { + throw new SmartherGatewayException(e.getMessage(), e); + } + } + + /** + * Manages a generic call to the API gateway using the given authorization access token. + * Retries the call if the access token is expired (refreshing it on behalf of further calls). + * + * @param call + * the http call to make + * @param accessToken + * the authorization access token to use + * + * @return the response received from the API gateway + * + * @throws {@link OAuthException} + * in case of issues during the OAuth process + * @throws {@link OAuthResponseException} + * in case of response issues during the OAuth process + * @throws {@link IOException} + * in case of I/O issues of some sort + */ + private ContentResponse requestWithRetry(final Function call, final String accessToken) + throws OAuthException, OAuthResponseException, IOException { + try { + return this.connector.request(call, this.oAuthSubscriptionKey, BEARER + accessToken); + } catch (SmartherTokenExpiredException e) { + // Retry with new access token + try { + return this.connector.request(call, this.oAuthSubscriptionKey, + BEARER + this.oAuthClientService.refreshToken().getAccessToken()); + } catch (SmartherTokenExpiredException ex) { + // This should never happen in normal conditions + throw new SmartherAuthorizationException(String.format("Cannot refresh token: %s", ex.getMessage())); + } + } + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/SmartherApiConnector.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/SmartherApiConnector.java new file mode 100644 index 0000000000000..a8f33a4e7e91c --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/SmartherApiConnector.java @@ -0,0 +1,336 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api; + +import static org.eclipse.jetty.http.HttpStatus.*; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherAuthorizationException; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherInvalidResponseException; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherSubscriptionAlreadyExistsException; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherTokenExpiredException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +/** + * The {@code SmartherApiConnector} class is used to perform the actual call to the API gateway. + * It handles the returned http status codes and the error codes eventually returned by the API gateway itself. + * + * Response mappings: + *
    + *
  • Plants : 200, 204, 400, 401, 404, 408, 469, 470, 500
  • + *
  • Topology : 200, 400, 401, 404, 408, 469, 470, 500
  • + *
  • Measures : 200, 400, 401, 404, 408, 469, 470, 500
  • + *
  • ProgramList : 200, 400, 401, 404, 408, 469, 470, 500
  • + *
  • Get Status : 200, 400, 401, 404, 408, 469, 470, 500
  • + *
  • Set Status : 200, 400, 401, 404, 408, 430, 469, 470, 486, 500
  • + *
  • Get Subscriptions : 200, 204, 400, 401, 404, 500
  • + *
  • Subscribe : 201, 400, 401, 404, 409, 500
  • + *
  • Delete Subscription : 200, 400, 401, 404, 500
  • + *
+ * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherApiConnector { + + private static final String RETRY_AFTER_HEADER = "Retry-After"; + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String SUBSCRIPTION_HEADER = "Ocp-Apim-Subscription-Key"; + + private static final String ERROR_CODE = "statusCode"; + private static final String ERROR_MESSAGE = "message"; + private static final String TOKEN_EXPIRED = "expired"; + private static final String AUTHORIZATION_ERROR = "error_description"; + + private static final int HTTP_CLIENT_TIMEOUT_SECONDS = 10; + private static final int HTTP_CLIENT_RETRY_COUNT = 5; + + // Set Chronothermostat Status > Wrong input parameters + private static final int WRONG_INPUT_PARAMS_430 = 430; + // Official application password expired: password used in the Thermostat official app is expired. + private static final int APP_PASSWORD_EXPIRED_469 = 469; + // Official application terms and conditions expired: terms and conditions for Thermostat official app are expired. + private static final int APP_TERMS_EXPIRED_470 = 470; + // Set Chronothermostat Status > Busy visual user interface + private static final int BUSY_VISUAL_UI_486 = 486; + + private final Logger logger = LoggerFactory.getLogger(SmartherApiConnector.class); + + private final JsonParser parser = new JsonParser(); + private final HttpClient httpClient; + private final ScheduledExecutorService scheduler; + + /** + * Constructs a {@code SmartherApiConnector} to the API gateway with the specified scheduler and http client. + * + * @param scheduler + * the scheduler to be used to reschedule calls when rate limit exceeded or call not succeeded + * @param httpClient + * the http client to be used to make http calls to the API gateway + */ + public SmartherApiConnector(ScheduledExecutorService scheduler, HttpClient httpClient) { + this.scheduler = scheduler; + this.httpClient = httpClient; + } + + /** + * Performs a call to the API gateway and returns the raw response. + * + * @param requester + * the function to construct the request, using the http client that is passed as argument to the + * function itself + * @param subscription + * the subscription string to be used in the call {@code Subscription} header + * @param authorization + * the authorization string to be used in the call {@code Authorization} header + * + * @return the raw response returned by the API gateway + * + * @throws {@link SmartherGatewayException} + * if the call failed due to an issue with the API gateway + */ + public ContentResponse request(Function requester, String subscription, String authorization) + throws SmartherGatewayException { + final Caller caller = new Caller(requester, subscription, authorization); + + try { + return caller.call().get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new SmartherGatewayException("Thread interrupted"); + } catch (ExecutionException e) { + final Throwable cause = e.getCause(); + + if (cause instanceof SmartherGatewayException) { + throw (SmartherGatewayException) cause; + } else { + throw new SmartherGatewayException(e.getMessage(), e); + } + } + } + + /** + * The {@code Caller} class represents the handler to make calls to the API gateway. + * In case of rate limiting or not finished jobs, it will retry a number of times in a specified timeframe then + * gives up with an exception. + * + * @author Fabio Possieri - Initial contribution + */ + private class Caller { + private final Function requester; + private final String subscription; + private final String authorization; + + private final CompletableFuture future = new CompletableFuture<>(); + private int delaySeconds; + private int attempts; + + /** + * Constructs a {@code Caller} to the API gateway with the specified requester, subscription and authorization. + * + * @param requester + * the function to construct the request, using the http client that is passed as argument to the + * function itself + * @param subscription + * the subscription string to be used in the call {@code Subscription} header + * @param authorization + * the authorization string to be used in the call {@code Authorization} header + */ + public Caller(Function requester, String subscription, String authorization) { + this.requester = requester; + this.subscription = subscription; + this.authorization = authorization; + } + + /** + * Performs the request as a {@link CompletableFuture}, setting its state once finished. + * The original caller should call the {@code get} method on the Future to wait for the call to finish. + * The first attempt is not scheduled so, if the first call succeeds, the {@code get} method directly returns + * the value. This method is rescheduled in case the call is to be retried. + * + * @return the {@link CompletableFuture} holding the call + */ + public CompletableFuture call() { + attempts++; + try { + final boolean success = processResponse(requester.apply(httpClient) + .header(SUBSCRIPTION_HEADER, subscription).header(AUTHORIZATION_HEADER, authorization) + .timeout(HTTP_CLIENT_TIMEOUT_SECONDS, TimeUnit.SECONDS).send()); + + if (!success) { + if (attempts < HTTP_CLIENT_RETRY_COUNT) { + logger.debug("API Gateway call attempt: {}", attempts); + + scheduler.schedule(this::call, delaySeconds, TimeUnit.SECONDS); + } else { + logger.debug("Giving up on accessing API Gateway. Check network connectivity!"); + future.completeExceptionally(new SmartherGatewayException( + String.format("Could not reach the API Gateway after %s retries.", attempts))); + } + } + } catch (ExecutionException e) { + future.completeExceptionally(e.getCause()); + } catch (SmartherGatewayException e) { + future.completeExceptionally(e); + } catch (RuntimeException | TimeoutException e) { + future.completeExceptionally(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + future.completeExceptionally(e); + } + return future; + } + + /** + * Processes the response from the API gateway call and handles the http status codes. + * + * @param response + * the response content returned by the API gateway + * + * @return {@code true} if the call was successful, {@code false} if the call failed in a way that can be + * retried + * + * @throws {@link SmartherGatewayException} + * if the call failed due to an irrecoverable issue and cannot be retried (user should be informed) + */ + private boolean processResponse(ContentResponse response) throws SmartherGatewayException { + boolean success = false; + + logger.debug("Response Code: {}", response.getStatus()); + if (logger.isTraceEnabled()) { + logger.trace("Response Data: {}", response.getContentAsString()); + } + switch (response.getStatus()) { + case OK_200: + case CREATED_201: + case NO_CONTENT_204: + case NOT_MODIFIED_304: + future.complete(response); + success = true; + break; + + case ACCEPTED_202: + logger.debug( + "API Gateway returned error status 202 (the request has been accepted for processing, but the processing has not been completed)"); + future.complete(response); + success = true; + break; + + case FORBIDDEN_403: + // Process for authorization error, and logging. + processErrorState(response); + future.complete(response); + success = true; + break; + + case BAD_REQUEST_400: + case NOT_FOUND_404: + case REQUEST_TIMEOUT_408: + case WRONG_INPUT_PARAMS_430: + case APP_PASSWORD_EXPIRED_469: + case APP_TERMS_EXPIRED_470: + case BUSY_VISUAL_UI_486: + case INTERNAL_SERVER_ERROR_500: + throw new SmartherGatewayException(processErrorState(response)); + + case UNAUTHORIZED_401: + throw new SmartherAuthorizationException(processErrorState(response)); + + case CONFLICT_409: + // Subscribe to C2C notifications > Subscription already exists. + throw new SmartherSubscriptionAlreadyExistsException(processErrorState(response)); + + case TOO_MANY_REQUESTS_429: + // Response Code 429 means requests rate limits exceeded. + final String retryAfter = response.getHeaders().get(RETRY_AFTER_HEADER); + logger.debug( + "API Gateway returned error status 429 (rate limit exceeded - retry after {} seconds, decrease polling interval of bridge, going to sleep...)", + retryAfter); + delaySeconds = Integer.parseInt(retryAfter); + break; + + case BAD_GATEWAY_502: + case SERVICE_UNAVAILABLE_503: + default: + throw new SmartherGatewayException(String.format("API Gateway returned error status %s (%s)", + response.getStatus(), HttpStatus.getMessage(response.getStatus()))); + } + return success; + } + + /** + * Processes the responded content if the status code indicated an error. + * + * @param response + * the response content returned by the API gateway + * + * @return the error message extracted from the response content + * + * @throws {@link SmartherTokenExpiredException} + * if the authorization access token used to communicate with the API gateway has expired + * @throws {@link SmartherAuthorizationException} + * if a generic authorization issue with the API gateway has occurred + * @throws {@link SmartherInvalidResponseException} + * if the response received from the API gateway cannot be parsed + */ + private String processErrorState(ContentResponse response) + throws SmartherTokenExpiredException, SmartherAuthorizationException, SmartherInvalidResponseException { + try { + final JsonElement element = parser.parse(response.getContentAsString()); + + if (element.isJsonObject()) { + final JsonObject object = element.getAsJsonObject(); + if (object.has(ERROR_CODE) && object.has(ERROR_MESSAGE)) { + final String message = object.get(ERROR_MESSAGE).getAsString(); + + // Bad request can be anything, from authorization problems to plant or module problems. + // Therefore authorization type errors are filtered and handled differently. + logger.debug("Bad request: {}", message); + if (message.contains(TOKEN_EXPIRED)) { + throw new SmartherTokenExpiredException(message); + } else { + return message; + } + } else if (object.has(AUTHORIZATION_ERROR)) { + final String errorDescription = object.get(AUTHORIZATION_ERROR).getAsString(); + throw new SmartherAuthorizationException(errorDescription); + } + } + logger.debug("Unknown response: {}", response); + return "Unknown response"; + } catch (JsonSyntaxException e) { + logger.warn("Response was not json: ", e); + throw new SmartherInvalidResponseException(e.getMessage()); + } + } + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Chronothermostat.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Chronothermostat.java new file mode 100644 index 0000000000000..1667ed681ec29 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Chronothermostat.java @@ -0,0 +1,234 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.LoadState; +import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.MeasureUnit; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherIllegalPropertyValueException; +import org.openhab.binding.bticinosmarther.internal.util.DateUtil; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@code Chronothermostat} class defines the dto for Smarther API chronothermostat object. + * + * @author Fabio Possieri - Initial contribution + */ +public class Chronothermostat { + + private static final String TIME_FOREVER = "Forever"; + + private String function; + private String mode; + @SerializedName("setPoint") + private Measure setPointTemperature; + private List programs; + @SerializedName("temperatureFormat") + private String temperatureFormat; + @SerializedName("loadState") + private String loadState; + @SerializedName("activationTime") + private String activationTime; + private String time; + private Sensor thermometer; + private Sensor hygrometer; + private boolean online; + private Sender sender; + + /** + * Returns the operational function of this chronothermostat module. + * + * @return a string containing the module operational function + */ + public String getFunction() { + return function; + } + + /** + * Returns the operational mode of this chronothermostat module. + * + * @return a string containing the module operational mode + */ + public String getMode() { + return mode; + } + + /** + * Returns the operational setpoint temperature of this chronothermostat module. + * + * @return a {@link Measure} object representing the module operational setpoint temperature + */ + public Measure getSetPointTemperature() { + return setPointTemperature; + } + + /** + * Returns the list of programs registered on this chronothermostat module. + * + * @return the list of registered programs, or an empty list in case of no programs available + */ + public List getPrograms() { + return (programs != null) ? programs : Collections.emptyList(); + } + + /** + * Returns the operational temperature format of this chronothermostat module. + * + * @return a string containing the module operational temperature format + */ + public String getTemperatureFormat() { + return temperatureFormat; + } + + /** + * Returns the operational temperature format of this chronothermostat module. + * + * @return a {@link MeasureUnit} object representing the module operational temperature format + * + * @throws {@link SmartherIllegalPropertyValueException} + * if the measure internal raw unit cannot be mapped to any valid measure unit + */ + public MeasureUnit getTemperatureFormatUnit() throws SmartherIllegalPropertyValueException { + return MeasureUnit.fromValue(temperatureFormat); + } + + /** + * Returns the operational load state of this chronothermostat module. + * + * @return a string containing the module operational load state + */ + public String getLoadState() { + return loadState; + } + + /** + * Tells whether the load state of this chronothermostat module is "active" (i.e. module is turned on). + * + * @return {@code true} if the load state is active, {@code false} otherwise + * + * @throws {@link SmartherIllegalPropertyValueException} + * if the load state internal raw value cannot be mapped to any valid load state enum value + */ + public boolean isActive() throws SmartherIllegalPropertyValueException { + return LoadState.fromValue(loadState).isActive(); + } + + /** + * Returns the operational activation time of this chronothermostat module. + * + * @return a string containing the module operational activation time + */ + public String getActivationTime() { + return activationTime; + } + + /** + * Returns a label for the operational activation time of this chronothermostat module. + * + * @return a string containing the module operational activation time label, or {@code null} if the activation time + * cannot be parsed to a valid date/time + */ + public @Nullable String getActivationTimeLabel() { + String timeLabel = TIME_FOREVER; + if (activationTime != null) { + try { + final ZonedDateTime dateActivationTime = DateUtil.parseZonedTime(activationTime, DTF_DATETIME_EXT); + final ZonedDateTime dateTomorrow = DateUtil.getZonedStartOfDay(1, dateActivationTime.getZone()); + + if (dateActivationTime.isBefore(dateTomorrow)) { + timeLabel = DateUtil.format(dateActivationTime, DTF_TODAY); + } else if (dateActivationTime.isBefore(dateTomorrow.plusDays(1))) { + timeLabel = DateUtil.format(dateActivationTime, DTF_TOMORROW); + } else { + timeLabel = DateUtil.format(dateActivationTime, DTF_DAY_HHMM); + } + } catch (DateTimeParseException e) { + timeLabel = null; + } + } + return timeLabel; + } + + /** + * Returns the current time (clock) of this chronothermostat module. + * + * @return a string containing the module current time + */ + public String getTime() { + return time; + } + + /** + * Returns the thermometer sensor of this chronothermostat module. + * + * @return the thermometer sensor of this module + */ + public Sensor getThermometer() { + return thermometer; + } + + /** + * Returns the hygrometer sensor of this chronothermostat module. + * + * @return the hygrometer sensor of this module + */ + public Sensor getHygrometer() { + return hygrometer; + } + + /** + * Tells whether this module is online. + * + * @return {@code true} if the module is online, {@code false} otherwise + */ + public boolean isOnline() { + return online; + } + + /** + * Returns the sender associated with this chronothermostat module. + * + * @return a {@link Sender} object representing the sender associated with this module, or {@code null} in case of + * no sender information available + */ + public @Nullable Sender getSender() { + return sender; + } + + /** + * Returns the operational program of this chronothermostat module. + * + * @return a {@link Program} object representing the module operational program, or {@code null} in case of no + * program currently set for this module + */ + public @Nullable Program getProgram() { + return (programs != null && !programs.isEmpty()) ? programs.get(0) : null; + } + + @Override + public String toString() { + return String.format( + "function=%s, mode=%s, setPointTemperature=[%s], programs=%s, temperatureFormat=%s, loadState=%s, time=%s, activationTime=%s, thermometer=[%s], hygrometer=[%s], online=%s, sender=[%s]", + function, mode, setPointTemperature, programs, temperatureFormat, loadState, time, activationTime, + thermometer, hygrometer, online, sender); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Enums.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Enums.java new file mode 100644 index 0000000000000..14360ce3133aa --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Enums.java @@ -0,0 +1,268 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.unit.ImperialUnits; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherIllegalPropertyValueException; + +/** + * The {@code Enums} class represents a container for enums related to Smarther API. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class Enums { + + /** + * The {@code Function} enum maps the values of chronothermostat operation function. + */ + public enum Function implements TypeWithStringProperty { + HEATING("HEATING"), + COOLING("COOLING"); + + private final String value; + + Function(String value) { + this.value = value; + } + + @Override + public String getValue() { + return value; + } + + /** + * Returns a {@code Function} enum value from the given raw value. + * + * @param value + * the raw value to get an enum value from + * + * @return the enum value representing the given raw value + * + * @throws {@link SmartherIllegalPropertyValueException} + * if the raw value cannot be mapped to any valid enum value + */ + public static Function fromValue(String value) throws SmartherIllegalPropertyValueException { + return lookup(Function.class, value); + } + } + + /** + * The {@code Mode} enum maps the values of chronothermostat operation mode. + */ + public enum Mode implements TypeWithStringProperty { + AUTOMATIC("AUTOMATIC"), + MANUAL("MANUAL"), + BOOST("BOOST"), + OFF("OFF"), + PROTECTION("PROTECTION"); + + private final String value; + + Mode(String value) { + this.value = value; + } + + @Override + public String getValue() { + return value; + } + + /** + * Returns a {@code Mode} enum value from the given raw value. + * + * @param value + * the raw value to get an enum value from + * + * @return the enum value representing the given raw value + * + * @throws {@link SmartherIllegalPropertyValueException} + * if the raw value cannot be mapped to any valid enum value + */ + public static Mode fromValue(String value) throws SmartherIllegalPropertyValueException { + return lookup(Mode.class, value); + } + } + + /** + * The {@code LoadState} enum maps the values of chronothermostat operation load state. + */ + public enum LoadState implements TypeWithStringProperty { + ACTIVE("ACTIVE"), + INACTIVE("INACTIVE"); + + private final String value; + + LoadState(String value) { + this.value = value; + } + + @Override + public String getValue() { + return value; + } + + /** + * Tells whether the load state value is "active". + * + * @return {@code true} if the load state value is "active", {@code false} otherwise + */ + public boolean isActive() { + return ACTIVE.getValue().equals(value); + } + + /** + * Returns a {@code LoadState} enum value from the given raw value. + * + * @param value + * the raw value to get an enum value from + * + * @return the enum value representing the given raw value + * + * @throws {@link SmartherIllegalPropertyValueException} + * if the raw value cannot be mapped to any valid enum value + */ + public static LoadState fromValue(String value) throws SmartherIllegalPropertyValueException { + return lookup(LoadState.class, value); + } + } + + /** + * The {@code MeasureUnit} enum maps the values of managed measure unit. + */ + public enum MeasureUnit implements TypeWithStringProperty { + CELSIUS("C"), + FAHRENHEIT("F"), + PERCENTAGE("%"), + DIMENSIONLESS(""); + + private final String value; + + MeasureUnit(String value) { + this.value = value; + } + + @Override + public String getValue() { + return value; + } + + /** + * Returns a {@code MeasureUnit} enum value for the given measure {@link Unit}. + * + * @param unit + * the measure unit to get an enum value for + * + * @return the enum value representing the given measure unit + */ + public static MeasureUnit fromUnit(Unit unit) { + if (unit == SIUnits.CELSIUS) { + return CELSIUS; + } else if (unit == ImperialUnits.FAHRENHEIT) { + return FAHRENHEIT; + } else if (unit == SmartHomeUnits.PERCENT) { + return PERCENTAGE; + } else { + return DIMENSIONLESS; + } + } + + /** + * Returns a {@code MeasureUnit} enum value from the given raw value. + * + * @param value + * the raw value to get an enum value from + * + * @return the enum value representing the given raw value + * + * @throws {@link SmartherIllegalPropertyValueException} + * if the raw value cannot be mapped to any valid enum value + */ + public static MeasureUnit fromValue(String value) throws SmartherIllegalPropertyValueException { + return lookup(MeasureUnit.class, value); + } + } + + /** + * The {@code BoostTime} enum maps the time values of chronothermostat boost mode. + */ + public enum BoostTime implements TypeWithIntProperty { + MINUTES_30(30), + MINUTES_60(60), + MINUTES_90(90); + + private final int value; + + BoostTime(int value) { + this.value = value; + } + + @Override + public int getValue() { + return value; + } + + /** + * Returns a {@code BoostTime} enum value from the given raw value. + * + * @param value + * the raw value to get an enum value from + * + * @return the enum value representing the given raw value + * + * @throws {@link SmartherIllegalPropertyValueException} + * if the raw value cannot be mapped to any valid enum value + */ + public static BoostTime fromValue(int value) throws SmartherIllegalPropertyValueException { + return lookup(BoostTime.class, value); + } + } + + // ------------------------------ + // UTILITY INTERFACES AND METHODS + // ------------------------------ + + interface TypeWithIntProperty { + int getValue(); + } + + public static & TypeWithIntProperty> E lookup(Class en, int value) + throws SmartherIllegalPropertyValueException { + for (E constant : en.getEnumConstants()) { + if (constant.getValue() == value) { + return constant; + } + } + throw new SmartherIllegalPropertyValueException(en.getSimpleName(), String.valueOf(value)); + } + + interface TypeWithStringProperty { + String getValue(); + } + + public static & TypeWithStringProperty> E lookup(Class en, String value) + throws SmartherIllegalPropertyValueException { + for (E constant : en.getEnumConstants()) { + if (constant.getValue().equals(value)) { + return constant; + } + } + throw new SmartherIllegalPropertyValueException(en.getSimpleName(), value); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Location.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Location.java new file mode 100644 index 0000000000000..15474483f8e3f --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Location.java @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.NAME_SEPARATOR; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.bticinosmarther.internal.util.StringUtil; + +/** + * The {@code Location} class defines the dto for Smarther API location object. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class Location { + + private String plantId; + private String name; + private @Nullable String subscriptionId; + private @Nullable String endpointUrl; + + /** + * Constructs a new {@code Location} with the given plant and subscription. + * + * @param plant + * the location plant to use + * @param subscription + * the notification subscription endpoint to use, may be {@code null} + */ + private Location(Plant plant, @Nullable Subscription subscription) { + super(); + this.plantId = plant.getId(); + this.name = plant.getName(); + if (subscription != null) { + this.subscriptionId = subscription.getSubscriptionId(); + this.endpointUrl = subscription.getEndpointUrl(); + } + } + + /** + * Returns a new {@code Location} with the given plant and subscription. + * + * @param plant + * the location plant to use + * @param subscription + * the notification subscription endpoint to use, may be {@code null} + * + * @return the newly created Location object + */ + public static Location fromPlant(Plant plant, @Nullable Subscription subscription) { + return new Location(plant, subscription); + } + + /** + * Returns a new {@code Location} with the given plant and no subscription. + * + * @param plant + * the location plant to use + * + * @return the newly created Location object + */ + public static Location fromPlant(Plant plant) { + return new Location(plant, null); + } + + /** + * Returns a new {@code Location} with the given plant and optional subscription. + * + * @param plant + * the location plant to use + * @param subscription + * the optional notification subscription endpoint to use, may contain no subscription + * + * @return the newly created Location object + */ + public static Location fromPlant(Plant plant, Optional subscription) { + return (subscription.isPresent()) ? new Location(plant, subscription.get()) : new Location(plant, null); + } + + /** + * Returns the plant identifier associated with this location. + * + * @return a string containing the plant identifier + */ + public String getPlantId() { + return plantId; + } + + /** + * Returns the plant name associated with this location. + * + * @return a string containing the plant name + */ + public String getName() { + return name; + } + + /** + * Tells whether the location has an associated subscription. + * + * @return {@code true} if the location has a subscription, {@code false} otherwise + */ + public boolean hasSubscription() { + return !StringUtil.isBlank(subscriptionId); + } + + /** + * Sets the notification subscription details for the location. + * + * @param subscriptionId + * the subscription identifier to use + * @param endpointUrl + * the notification endpoint to use + */ + public void setSubscription(String subscriptionId, String endpointUrl) { + this.subscriptionId = subscriptionId; + this.endpointUrl = endpointUrl; + } + + /** + * Unsets the notification subscription details for the location. + * I.e. resets all of its details to {@code null}. + */ + public void unsetSubscription() { + this.subscriptionId = null; + this.endpointUrl = null; + } + + /** + * Returns the notification subscription identifier for this location. + * + * @return a string containing the subscription identifier, may be {@code null} + */ + public @Nullable String getSubscriptionId() { + return subscriptionId; + } + + /** + * Returns the notification endpoint for this location. + * + * @return a string containing the notification endpoint, may be {@code null} + */ + public @Nullable String getEndpointUrl() { + return endpointUrl; + } + + /** + * Converts a list of {@link Location} objects into a string containing the location names, comma separated. + * + * @param locations + * the list of location objects to be converted, may be {@code null} + * + * @return a string containing the comma separated location names, or {@code null} if the list is {@code null} or + * empty. + */ + public static @Nullable String toNameString(@Nullable List locations) { + if (locations == null || locations.isEmpty()) { + return null; + } + return locations.stream().map(a -> String.valueOf(a.getName())).collect(Collectors.joining(NAME_SEPARATOR)); + } + + @Override + public String toString() { + return String.format("plantId=%s, name=%s, subscriptionId=%s, endpointUrl=%s", plantId, name, subscriptionId, + endpointUrl); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Measure.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Measure.java new file mode 100644 index 0000000000000..0d72c76861d3e --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Measure.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import java.util.Optional; + +import javax.measure.quantity.Dimensionless; +import javax.measure.quantity.Temperature; + +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.unit.ImperialUnits; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.MeasureUnit; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherIllegalPropertyValueException; +import org.openhab.binding.bticinosmarther.internal.util.StringUtil; + +import com.google.gson.annotations.SerializedName; + +import tec.uom.se.unit.Units; + +/** + * The {@code Measure} class defines the dto for Smarther API measure object. + * + * @author Fabio Possieri - Initial contribution + */ +public class Measure { + + @SerializedName("timeStamp") + private String timestamp; + private String value; + private String unit; + + public String getTimestamp() { + return timestamp; + } + + /** + * Returns the value of this measure. + * + * @return a string containing the measure value + */ + public String getValue() { + return value; + } + + /** + * Returns the measure unit of this measure. + * + * @return a string containing the measure unit + */ + public String getUnit() { + return unit; + } + + /** + * Returns the measure unit of this measure. + * + * @return a {@link MeasureUnit} object representing the measure unit + * + * @throws {@link SmartherIllegalPropertyValueException} + * if the measure internal raw unit cannot be mapped to any valid measure unit + */ + public MeasureUnit getMeasureUnit() throws SmartherIllegalPropertyValueException { + return MeasureUnit.fromValue(unit); + } + + /** + * Returns the value and measure unit of this measure as a combined {@link State} object. + * + * @return the value and measure unit + * + * @throws {@link SmartherIllegalPropertyValueException} + * if the measure internal raw unit cannot be mapped to any valid measure unit + */ + public State toState() throws SmartherIllegalPropertyValueException { + State state = UnDefType.UNDEF; + final Optional optValue = (StringUtil.isBlank(value)) ? Optional.empty() + : Optional.of(Double.parseDouble(value)); + + switch (MeasureUnit.fromValue(unit)) { + case CELSIUS: + state = optValue.map(t -> new QuantityType(new DecimalType(t), SIUnits.CELSIUS)) + .orElse(UnDefType.UNDEF); + break; + case FAHRENHEIT: + state = optValue + .map(t -> new QuantityType(new DecimalType(t), ImperialUnits.FAHRENHEIT)) + .orElse(UnDefType.UNDEF); + break; + case PERCENTAGE: + state = optValue.map(t -> new QuantityType(new DecimalType(t), Units.PERCENT)) + .orElse(UnDefType.UNDEF); + break; + case DIMENSIONLESS: + state = optValue.map(t -> new DecimalType(t)).orElse(UnDefType.UNDEF); + } + + return state; + } + + @Override + public String toString() { + return (StringUtil.isBlank(timestamp)) ? String.format("value=%s, unit=%s", value, unit) + : String.format("value=%s, unit=%s, timestamp=%s", value, unit, timestamp); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Module.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Module.java new file mode 100644 index 0000000000000..b08b3d397945e --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Module.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import org.openhab.binding.bticinosmarther.internal.util.StringUtil; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@code Module} class defines the dto for Smarther API chronothermostat module object. + * + * @author Fabio Possieri - Initial contribution + */ +public class Module { + + @SerializedName("device") + private String deviceType; + private String id; + private String name; + + /** + * Returns the device type of the chronothermostat module. + * + * @return a string containing the module device type + */ + public String getDeviceType() { + return StringUtil.capitalizeAll(deviceType); + } + + /** + * Returns the identifier of the chronothermostat module. + * + * @return a string containing the module identifier + */ + public String getId() { + return id; + } + + /** + * Returns the chronothermostat module reference label (i.e. the module "name"). + * + * @return a string containing the module reference label + */ + public String getName() { + return name; + } + + @Override + public String toString() { + return String.format("id=%s, name=%s, type=%s", id, name, deviceType); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/ModuleRef.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/ModuleRef.java new file mode 100644 index 0000000000000..d4bcd8e32c1ce --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/ModuleRef.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +/** + * The {@code ModuleRef} class defines the dto for Smarther API chronothermostat module reference object. + * + * @author Fabio Possieri - Initial contribution + */ +public class ModuleRef { + + private String id; + + /** + * Returns the identifier of the chronothermostat module. + * + * @return a string containing the module identifier + */ + public String getId() { + return id; + } + + @Override + public String toString() { + return String.format("id=%s", id); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/ModuleStatus.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/ModuleStatus.java new file mode 100644 index 0000000000000..7fd80c8a2fa74 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/ModuleStatus.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import java.util.List; + +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@code ModuleStatus} class defines the dto for Smarther API module status object. + * + * @author Fabio Possieri - Initial contribution + */ +public class ModuleStatus { + + private List chronothermostats; + + /** + * Returns the chronothermostat details of this module status. + * + * @return the chronothermostat details + */ + public List getChronothermostats() { + return chronothermostats; + } + + /** + * Returns the first chronothermostat item contained in this module status. + * + * @return the first chronothermostat item, or {@code null} in case of no item found + */ + public @Nullable Chronothermostat toChronothermostat() { + return (!chronothermostats.isEmpty() && chronothermostats.get(0) != null) ? chronothermostats.get(0) : null; + } + + @Override + public String toString() { + return String.format("chronothermostats=[%s]", chronothermostats); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Modules.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Modules.java new file mode 100644 index 0000000000000..cb83f8f056dc1 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Modules.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.NAME_SEPARATOR; + +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@code Modules} class defines the dto for Smarther API list of modules. + * + * @author Fabio Possieri - Initial contribution + */ +public class Modules { + + private List modules; + + /** + * Returns the list of modules contained in this object. + * + * @return the list of modules + */ + public @Nullable List getModules() { + return modules; + } + + /** + * Converts a list of {@link Module} objects into a string containing the module names, comma separated. + * + * @param modules + * the list of module objects to be converted, may be {@code null} + * + * @return a string containing the comma separated module names, or {@code null} if the list is {@code null} or + * empty. + */ + public static @Nullable String toNameString(@Nullable List modules) { + if (modules == null || modules.isEmpty()) { + return null; + } + return modules.stream().map(a -> String.valueOf(a.getName())).collect(Collectors.joining(NAME_SEPARATOR)); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Notification.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Notification.java new file mode 100644 index 0000000000000..c5257eae18910 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Notification.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@code Notification} class defines the dto for Smarther API notification object. + * + * @author Fabio Possieri - Initial contribution + */ +public class Notification { + + private String id; + @SerializedName("eventType") + private String eventType; + private String subject; + @SerializedName("eventTime") + private String eventTime; + private ModuleStatus data; + + /** + * Returns the identifier of this notification. + * + * @return a string containing the notification identifier + */ + public String getId() { + return id; + } + + /** + * Returns the event type of this notification. + * + * @return a string containing the notification event type + */ + public String getEventType() { + return eventType; + } + + /** + * Returns the subject of this notification. + * + * @return a string containing the notification subject + */ + public String getSubject() { + return subject; + } + + /** + * Returns the event time of this notification. + * + * @return a string containing the notification event time + */ + public String getEventTime() { + return eventTime; + } + + /** + * Returns the module status data (i.e. the payload) of this notification. + * + * @return the module status data, or {@code null} in case of no data found + */ + public @Nullable ModuleStatus getData() { + return data; + } + + /** + * Returns the chronothermostat details of this notification. + * + * @return the chronothermostat details, or {@code null} in case of no data found + */ + public @Nullable Chronothermostat getChronothermostat() { + if (data != null) { + return data.toChronothermostat(); + } + return null; + } + + /** + * Returns the sender details of this notification. + * + * @return the sender details, or {@code null} in case of no data found + */ + public @Nullable Sender getSender() { + if (data != null) { + final Chronothermostat chronothermostat = data.toChronothermostat(); + if (chronothermostat != null) { + return chronothermostat.getSender(); + } + } + return null; + } + + @Override + public String toString() { + return String.format("id=%s, eventType=%s, subject=%s, eventTime=%s, data=[%s]", id, eventType, subject, + eventTime, data); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Plant.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Plant.java new file mode 100644 index 0000000000000..cbdf90a699dcf --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Plant.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import java.util.List; + +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@code Plant} class defines the dto for Smarther API plant object. + * + * @author Fabio Possieri - Initial contribution + */ +public class Plant { + + private String id; + private String name; + private List modules; + + /** + * Returns the identifier of the plant. + * + * @return a string containing the plant identifier + */ + public String getId() { + return id; + } + + /** + * Returns the plant reference label (i.e. the plant "name"). + * + * @return a string containing the plant reference label + */ + public String getName() { + return name; + } + + /** + * Returns the list of chronothermostat modules of the plant. + * + * @return the list of chronothermostat modules of the plant, or {@code null} in case the plant has no modules + */ + public @Nullable List getModules() { + return modules; + } + + @Override + public String toString() { + return String.format("id=%s, name=%s", id, name); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/PlantRef.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/PlantRef.java new file mode 100644 index 0000000000000..d8537fe3f9d9a --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/PlantRef.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +/** + * The {@code PlantRef} class defines the dto for Smarther API plant reference object. + * + * @author Fabio Possieri - Initial contribution + */ +public class PlantRef { + + private String id; + private ModuleRef module; + + /** + * Returns the identifier of the plant. + * + * @return a string containing the plant identifier + */ + public String getId() { + return id; + } + + /** + * Returns the chronothermostat reference inside the plant. + * + * @return a {@link ModuleRef} object representing the chronothermostat module reference + */ + public ModuleRef getModule() { + return module; + } + + @Override + public String toString() { + return String.format("id=%s, module=[%s]", id, module); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Plants.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Plants.java new file mode 100644 index 0000000000000..340477f2c7300 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Plants.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import java.util.List; + +/** + * The {@code Plants} class defines the dto for Smarther API list of plants. + * + * @author Fabio Possieri - Initial contribution + */ +public class Plants { + + private List plants; + + /** + * Returns the list of plants contained in this object. + * + * @return the list of plants + */ + public List getPlants() { + return plants; + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Program.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Program.java new file mode 100644 index 0000000000000..9607a9d885cdc --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Program.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.DEFAULT_PROGRAM; + +/** + * The {@code Program} class defines the dto for Smarther API program object. + * + * @author Fabio Possieri - Initial contribution + */ +public class Program { + + private int number; + private String name; + + /** + * Returns the program number. + * + * @return the program number + */ + public int getNumber() { + return number; + } + + /** + * Returns the program reference label (i.e. the program "name"). + * + * @return a string containing the program reference label + */ + public String getName() { + return (number == 0) ? DEFAULT_PROGRAM : name; + } + + @Override + public String toString() { + return String.format("number=%d, name=%s", number, name); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Sender.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Sender.java new file mode 100644 index 0000000000000..7a61dad182837 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Sender.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@code Sender} class defines the dto for Smarther API sender object. + * + * @author Fabio Possieri - Initial contribution + */ +public class Sender { + + @SerializedName("addressType") + private String addressType; + private String system; + private PlantRef plant; + + /** + * Returns the sender address type. + * + * @return a string containing the sender address type + */ + public String getAddressType() { + return addressType; + } + + /** + * Returns the sender system. + * + * @return a string containing the sender system + */ + public String getSystem() { + return system; + } + + /** + * Returns the sender plant reference. + * + * @return a {@link PlantRef} object representing the sender plant reference + */ + public PlantRef getPlant() { + return plant; + } + + @Override + public String toString() { + return String.format("addressType=%s, system=%s, plant=[%s]", addressType, system, plant); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Sensor.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Sensor.java new file mode 100644 index 0000000000000..0b6f00b77cb8e --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Sensor.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import java.util.List; + +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherIllegalPropertyValueException; + +/** + * The {@code Sensor} class defines the dto for Smarther API sensor object. + * + * @author Fabio Possieri - Initial contribution + */ +public class Sensor { + + private List measures; + + /** + * Returns the list of measures this sensor takes. + * + * @return the measures this sensor takes, may be {@code null} + */ + public @Nullable List getMeasures() { + return measures; + } + + /** + * Returns the measure taken by this sensor at the given index. + * + * @param index + * the index to get the measure for + * + * @return the requested measure, or {@code null} in case of no measure found at given index + */ + public @Nullable Measure getMeasure(int index) { + return (measures != null && measures.size() > index) ? measures.get(index) : null; + } + + /** + * Returns the overall state of the sensor. + * + * @return a {@link State} object representing the overall state of the sensor + * + * @throws {@link SmartherIllegalPropertyValueException} + * if the sensor internal raw state cannot be mapped to any valid value + */ + public State toState() throws SmartherIllegalPropertyValueException { + final Measure measure = getMeasure(0); + return (measure != null) ? measure.toState() : UnDefType.UNDEF; + } + + @Override + public String toString() { + return String.format("measures=%s", measures); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Subscription.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Subscription.java new file mode 100644 index 0000000000000..be2e1d99972c4 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Subscription.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@code Subscription} class defines the dto for Smarther API notification subscription object. + * + * @author Fabio Possieri - Initial contribution + */ +public class Subscription { + + @SerializedName("plantId") + private String plantId; + @SerializedName("subscriptionId") + private String subscriptionId; + @SerializedName("EndPointUrl") + private String endpointUrl; + + /** + * Returns the identifier of the plant this subscription relates to. + * + * @return a string containing the plant identifier + */ + public String getPlantId() { + return plantId; + } + + /** + * Returns the notification subscription identifier. + * + * @return a string containing the subscription identifier + */ + public String getSubscriptionId() { + return subscriptionId; + } + + /** + * Returns the notification endpoint url this subscription maps to. + * + * @return a string containing the notification endpoint url + */ + public String getEndpointUrl() { + return endpointUrl; + } + + @Override + public String toString() { + return String.format("plantId=%s, id=%s, endpoint=%s", plantId, subscriptionId, endpointUrl); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Topology.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Topology.java new file mode 100644 index 0000000000000..6c83e2a3782fc --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/dto/Topology.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.dto; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@code Topology} class defines the dto for Smarther API topology object. + * + * @author Fabio Possieri - Initial contribution + */ +public class Topology { + + private Plant plant; + + /** + * Returns a {@link Plant} object representing the plant contained in this topology. + * + * @return the plant contained in this topology, or {@code null} if the topology has no plant + */ + public @Nullable Plant getPlant() { + return plant; + } + + /** + * Returns the list of chronothermostat modules contained in this topology. + * + * @return the list of chronothermostat modules contained in this topology, or an empty list in case the topology + * has no modules + */ + public List getModules() { + return (plant != null) ? plant.getModules() : new ArrayList<>(); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherAuthorizationException.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherAuthorizationException.java new file mode 100644 index 0000000000000..8af8deb07225f --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherAuthorizationException.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Signals that a generic OAuth2 authorization issue with API gateway has occurred. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherAuthorizationException extends SmartherGatewayException { + + private static final long serialVersionUID = 2608406239134276285L; + + /** + * Constructs a {@code SmartherAuthorizationException} with the specified detail message. + * + * @param message + * the error message returned from the API gateway + */ + public SmartherAuthorizationException(String message) { + super(message); + } + + /** + * Constructs a {@code SmartherAuthorizationException} with the specified detail message and cause. + * + * @param message + * the error message returned from the API gateway + * @param cause + * the cause (a null value is permitted, and indicates that the cause is nonexistent or unknown) + */ + public SmartherAuthorizationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherGatewayException.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherGatewayException.java new file mode 100644 index 0000000000000..b121f27d2372d --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherGatewayException.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.exception; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Signals that a generic communication issue with API gateway has occurred. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherGatewayException extends IOException { + + private static final long serialVersionUID = -3614645621941830547L; + + /** + * Constructs a {@code SmartherGatewayException} with the specified detail message. + * + * @param message + * the error message returned from the API gateway + */ + public SmartherGatewayException(String message) { + super(message); + } + + /** + * Constructs a {@code SmartherGatewayException} with the specified detail message and cause. + * + * @param message + * the error message returned from the API gateway + * @param cause + * the cause (a null value is permitted, and indicates that the cause is nonexistent or unknown) + */ + public SmartherGatewayException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a {@code SmartherGatewayException} with the specified cause and a detail message of + * {@code (cause==null ? null : cause.toString())} (which typically contains the class and detail message of + * {@code cause}). + * This constructor is useful for API gateway exceptions that are little more than wrappers for other throwables. + * + * @param cause + * the cause (a null value is permitted, and indicates that the cause is nonexistent or unknown) + */ + public SmartherGatewayException(Throwable cause) { + super(cause); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherIllegalPropertyValueException.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherIllegalPropertyValueException.java new file mode 100644 index 0000000000000..98a1945297fdc --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherIllegalPropertyValueException.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Signals that an "invalid property value" issue has occurred when deailng with enumerated type chronothermostat + * properties. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherIllegalPropertyValueException extends Exception { + + private static final long serialVersionUID = -2549779559688846805L; + + private static final String MSG_FORMAT = "'%s' = '%s'"; + + /** + * Constructs a {@code SmartherIllegalPropertyValueException} with the specified detail message. + * + * @param message + * the error message returned from the API gateway + */ + public SmartherIllegalPropertyValueException(String message) { + super(message); + } + + /** + * Constructs a {@code SmartherIllegalPropertyValueException} with the specified property name and invalid value + * returned by the API gateway. + * + * @param propertyName + * the property name that caused the issue + * @param invalidValue + * the invalid value returned by the API gateway for {@code PropertyName} + */ + public SmartherIllegalPropertyValueException(String propertyName, String invalidValue) { + super(String.format(MSG_FORMAT, propertyName, invalidValue)); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherInvalidResponseException.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherInvalidResponseException.java new file mode 100644 index 0000000000000..b4fc113d9925d --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherInvalidResponseException.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Signals that an "invalid response" messaging issue with API gateway has occurred. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherInvalidResponseException extends SmartherGatewayException { + + private static final long serialVersionUID = 3166922285185480855L; + + /** + * Constructs a {@code SmartherInvalidResponseException} with the specified detail message. + * + * @param message + * the error message returned from the API gateway + */ + public SmartherInvalidResponseException(String message) { + super(message); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherNotificationException.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherNotificationException.java new file mode 100644 index 0000000000000..b697b93b70361 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherNotificationException.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Signals that a generic C2C Webhook notification issue with API gateway has occurred. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherNotificationException extends RuntimeException { + + private static final long serialVersionUID = -634107708647244174L; + + /** + * Constructs a {@code SmartherNotificationException} with the specified detail message. + * + * @param message + * the error message returned from the API gateway + */ + public SmartherNotificationException(String message) { + super(message); + } + + /** + * Constructs a {@code SmartherNotificationException} with the specified detail message and cause. + * + * @param message + * the error message returned from the API gateway + * @param cause + * the cause (a null value is permitted, and indicates that the cause is nonexistent or unknown) + */ + public SmartherNotificationException(String message, Throwable exception) { + super(message, exception); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherSubscriptionAlreadyExistsException.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherSubscriptionAlreadyExistsException.java new file mode 100644 index 0000000000000..d8e516d1fbfc3 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherSubscriptionAlreadyExistsException.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Signals that a "subscription for given plant already exists" C2C Webhook issue with API gateway has occurred. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherSubscriptionAlreadyExistsException extends SmartherNotificationException { + + private static final long serialVersionUID = 5185321219105493105L; + + /** + * Constructs a {@code SmartherSubscriptionAlreadyExistsException} with the specified detail message. + * + * @param message + * the error message returned from the API gateway + */ + public SmartherSubscriptionAlreadyExistsException(String message) { + super(message); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherTokenExpiredException.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherTokenExpiredException.java new file mode 100644 index 0000000000000..3c706936e2714 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/api/exception/SmartherTokenExpiredException.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.api.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Signals that an "access token expired" OAuth2 authorization issue with API gateway has occurred. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherTokenExpiredException extends SmartherAuthorizationException { + + private static final long serialVersionUID = 6967072975936269922L; + + /** + * Constructs a {@code SmartherTokenExpiredException} with the specified detail message. + * + * @param message + * the error message returned from the API gateway + */ + public SmartherTokenExpiredException(String message) { + super(message); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/config/SmartherBridgeConfiguration.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/config/SmartherBridgeConfiguration.java new file mode 100644 index 0000000000000..14214002920e4 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/config/SmartherBridgeConfiguration.java @@ -0,0 +1,201 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.config; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The {@code SmartherBridgeConfiguration} class defines the internal configuration of a {@code SmartherBridgeHandler} + * instance. + * + * @author Fabio Possieri - Initial contribution + */ +public class SmartherBridgeConfiguration { + + private String subscriptionKey; + private String clientId; + private String clientSecret; + private boolean useNotifications; + private int statusRefreshPeriod; + private String notificationUrl; + private List notifications; + + /** + * Returns the Legrand/Bticino product subscription key. + * + * @return a string containing the subscription key + */ + public String getSubscriptionKey() { + return subscriptionKey; + } + + /** + * Sets the Legrand/Bticino product subscription key. + * + * @param subscriptionKey + * the new product subscription key + */ + public void setSubscriptionKey(String subscriptionKey) { + this.subscriptionKey = subscriptionKey; + } + + /** + * Returns the Legrand/Bticino user account client identifier. + * + * @return a string containing the client identifier + */ + public String getClientId() { + return clientId; + } + + /** + * Sets the Legrand/Bticino user account client identifier. + * + * @param clientId + * the new client identifier + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * Returns the Legrand/Bticino user account client secret. + * + * @return a string containing the client secret + */ + public String getClientSecret() { + return clientSecret; + } + + /** + * Sets the Legrand/Bticino user account client secret. + * + * @param clientSecret + * the new client secret + */ + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + /** + * Tells whether the Bridge subscribes to receive modules status notifications. + * + * @return {@code true} if the notifications are turned on, {@code false} otherwise + */ + public boolean isUseNotifications() { + return useNotifications; + } + + /** + * Sets whether the Bridge subscribes to receive modules status notifications. + * + * @param useNotifications + * {@code true} if the notifications are turned on, {@code false} otherwise + */ + public void setUseNotifications(boolean useNotifications) { + this.useNotifications = useNotifications; + } + + /** + * Returns the Bridge status refresh period (in minutes). + * + * @return the Bridge status refresh period + */ + public int getStatusRefreshPeriod() { + return statusRefreshPeriod; + } + + /** + * Sets the Bridge status refresh period (in minutes). + * + * @param statusRefreshPeriod + * the new Bridge status refresh period + */ + public void setStatusRefreshPeriod(int statusRefreshPeriod) { + this.statusRefreshPeriod = statusRefreshPeriod; + } + + /** + * Returns the notification url for this Bridge. + * + * @return a string containing the notification url + */ + public String getNotificationUrl() { + return notificationUrl; + } + + /** + * Sets the notification url for this Bridge. + * + * @param notificationUrl + * the new notification url + */ + public void setNotificationUrl(String notificationUrl) { + this.notificationUrl = notificationUrl; + } + + /** + * Adds a notification identifier to the Bridge notifications list. + * + * @param notificationId + * the notification identifier to add + * + * @return the new Bridge notifications list + */ + public List addNotification(String notificationId) { + if (notifications == null) { + notifications = new ArrayList<>(); + } + if (!notifications.contains(notificationId)) { + notifications.add(notificationId); + } + return notifications; + } + + /** + * Removes a notification identifier from the Bridge notifications list. + * + * @param notificationId + * the notification identifier to remove + * + * @return the new Bridge notifications list + */ + public List removeNotification(String notificationId) { + if (notifications != null) { + notifications.remove(notificationId); + } + return notifications; + } + + /** + * Returns the current Bridge notifications list. + * + * @return the current Bridge notifications list + */ + public List getNotifications() { + return (notifications != null) ? notifications : Collections.emptyList(); + } + + /** + * Sets a new Bridge notifications list. + * + * @param notifications + * the new notifications list to set + */ + public void setNotifications(List notifications) { + this.notifications = notifications; + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/config/SmartherModuleConfiguration.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/config/SmartherModuleConfiguration.java new file mode 100644 index 0000000000000..6429487a9eb73 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/config/SmartherModuleConfiguration.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.config; + +/** + * The {@code SmartherModuleConfiguration} class defines the internal configuration of a {@code SmartherModuleHandler} + * instance. + * + * @author Fabio Possieri - Initial contribution + */ +public class SmartherModuleConfiguration { + + private String plantId; + private String moduleId; + private boolean settingsAutoupdate; + private int programsRefreshPeriod; + private int numberOfEndDays; + private int statusRefreshPeriod; + + /** + * Returns the location plant identifier. + * + * @return a string containing the plant identifier + */ + public String getPlantId() { + return plantId; + } + + /** + * Sets the location plant identifier. + * + * @param plantId + * the new plant identifier + */ + public void setPlantId(String plantId) { + this.plantId = plantId; + } + + /** + * Returns the chronothermostat module identifier. + * + * @return a string containing the module identifier + */ + public String getModuleId() { + return moduleId; + } + + /** + * Sets the chronothermostat module identifier. + * + * @param moduleId + * the new module identifier + */ + public void setModuleId(String moduleId) { + this.moduleId = moduleId; + } + + /** + * Tells whether the Module settings are updated with its status. + * + * @return {@code true} if the settings are updated whenever the module status is updated, {@code false} if the + * settings are updated only upon module initialization + */ + public boolean isSettingsAutoupdate() { + return settingsAutoupdate; + } + + /** + * Sets whether the Module settings are updated with its status. + * + * @param settingsAutoupdate + * {@code true} if the settings are updated whenever the module status is updated, {@code false} if the + * settings are updated only upon module initialization + */ + public void setSettingsAutoupdate(boolean settingsAutoupdate) { + this.settingsAutoupdate = settingsAutoupdate; + } + + /** + * Returns the automatic mode programs refresh period (in hours). + * + * @return the automatic mode programs refresh period + */ + public int getProgramsRefreshPeriod() { + return programsRefreshPeriod; + } + + /** + * Sets the automatic mode programs refresh period (in hours). + * + * @param programsRefreshPeriod + * the new automatic mode programs refresh period + */ + public void setProgramsRefreshPeriod(int programsRefreshPeriod) { + this.programsRefreshPeriod = programsRefreshPeriod; + } + + /** + * Returns the number of end days to be displayed in manual mode. + * + * @return the number of end days to be displayed + */ + public int getNumberOfEndDays() { + return numberOfEndDays; + } + + /** + * Sets the number of end days to be displayed in manual mode. + * + * @param numberOfEndDays + * the new number of end days to be displayed + */ + public void setNumberOfEndDays(int numberOfEndDays) { + this.numberOfEndDays = numberOfEndDays; + } + + /** + * Returns the Module status refresh period (in minutes). + * + * @return the Module status refresh period + */ + public int getStatusRefreshPeriod() { + return statusRefreshPeriod; + } + + /** + * Sets the Module status refresh period (in minutes). + * + * @param statusRefreshPeriod + * the new Module status refresh period + */ + public void setStatusRefreshPeriod(int statusRefreshPeriod) { + this.statusRefreshPeriod = statusRefreshPeriod; + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/discovery/SmartherModuleDiscoveryService.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/discovery/SmartherModuleDiscoveryService.java new file mode 100644 index 0000000000000..845c2390ade10 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/discovery/SmartherModuleDiscoveryService.java @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.discovery; + +import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*; + +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; +import org.eclipse.smarthome.config.discovery.DiscoveryResult; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.openhab.binding.bticinosmarther.internal.account.SmartherAccountHandler; +import org.openhab.binding.bticinosmarther.internal.api.dto.Location; +import org.openhab.binding.bticinosmarther.internal.api.dto.Module; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@code SmartherModuleDiscoveryService} queries the Smarther API gateway to discover available Chronothermostat + * modules inside existing plants registered under the configured Bridges. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherModuleDiscoveryService extends AbstractDiscoveryService + implements DiscoveryService, ThingHandlerService { + + // Only modules can be discovered. A bridge must be manually added. + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_MODULE); + + private static final int DISCOVERY_TIME_SECONDS = 30; + + private static final String ID_SEPARATOR = "-"; + + private final Logger logger = LoggerFactory.getLogger(SmartherModuleDiscoveryService.class); + + private @Nullable SmartherAccountHandler bridgeHandler; + private @Nullable ThingUID bridgeUID; + + /** + * Constructs a {@code SmartherModuleDiscoveryService}. + */ + public SmartherModuleDiscoveryService() { + super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIME_SECONDS); + } + + @Override + public Set getSupportedThingTypes() { + return SUPPORTED_THING_TYPES_UIDS; + } + + @Override + public void activate() { + logger.debug("Bridge[{}] Activating chronothermostat discovery service", this.bridgeUID); + Map properties = new HashMap<>(); + properties.put(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY, Boolean.TRUE); + super.activate(properties); + } + + @Override + public void deactivate() { + logger.debug("Bridge[{}] Deactivating chronothermostat discovery service", this.bridgeUID); + removeOlderResults(new Date().getTime()); + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof SmartherAccountHandler) { + final SmartherAccountHandler localBridgeHandler = (SmartherAccountHandler) handler; + this.bridgeHandler = localBridgeHandler; + this.bridgeUID = localBridgeHandler.getUID(); + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return this.bridgeHandler; + } + + @Override + protected void startBackgroundDiscovery() { + logger.debug("Bridge[{}] Performing background discovery scan for chronothermostats", this.bridgeUID); + discoverChronothermostats(); + } + + @Override + protected void startScan() { + logger.debug("Bridge[{}] Starting discovery scan for chronothermostats", this.bridgeUID); + discoverChronothermostats(); + } + + @Override + public synchronized void abortScan() { + super.abortScan(); + } + + @Override + protected synchronized void stopScan() { + super.stopScan(); + removeOlderResults(getTimestampOfLastScan()); + } + + /** + * Discovers Chronothermostat devices for the given bridge handler. + */ + private synchronized void discoverChronothermostats() { + final SmartherAccountHandler localBridgeHandler = this.bridgeHandler; + if (localBridgeHandler != null) { + // If the bridge is not online no other thing devices can be found, so no reason to scan at this moment + if (localBridgeHandler.isOnline()) { + localBridgeHandler.getLocations() + .forEach(l -> localBridgeHandler.getLocationModules(l).forEach(m -> addDiscoveredDevice(l, m))); + } + } + } + + /** + * Creates a Chronothermostat module Thing based on the remotely discovered location and module. + * + * @param location + * the location containing the discovered module + * @param module + * the discovered module + */ + private void addDiscoveredDevice(Location location, Module module) { + Map properties = new HashMap<>(); + properties.put(PROPERTY_PLANT_ID, location.getPlantId()); + properties.put(PROPERTY_MODULE_ID, module.getId()); + properties.put(PROPERTY_MODULE_NAME, module.getName()); + properties.put(PROPERTY_DEVICE_TYPE, module.getDeviceType()); + + ThingUID thingUID = new ThingUID(THING_TYPE_MODULE, this.bridgeUID, getThingIdFromModule(module)); + + final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withBridge(this.bridgeUID) + .withProperties(properties).withRepresentationProperty(PROPERTY_MODULE_ID).withLabel(module.getName()) + .build(); + thingDiscovered(discoveryResult); + logger.debug("Bridge[{}] Chronothermostat with id '{}' and name '{}' added to Inbox with UID '{}'", + this.bridgeUID, module.getId(), module.getName(), thingUID); + } + + /** + * Generates the Thing identifier based on the Chronothermostat module identifier. + * + * @param module + * the Chronothermostat module to use + * + * @return a string containing the generated Thing identifier + */ + private String getThingIdFromModule(Module module) { + final String moduleId = module.getId(); + return moduleId.substring(0, moduleId.indexOf(ID_SEPARATOR)); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/factory/SmartherHandlerFactory.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/factory/SmartherHandlerFactory.java new file mode 100644 index 0000000000000..34ceaa910d888 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/factory/SmartherHandlerFactory.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.factory; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.smarthome.core.auth.client.oauth2.OAuthFactory; +import org.eclipse.smarthome.core.scheduler.CronScheduler; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.eclipse.smarthome.io.net.http.HttpClientFactory; +import org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants; +import org.openhab.binding.bticinosmarther.internal.account.SmartherAccountService; +import org.openhab.binding.bticinosmarther.internal.handler.SmartherBridgeHandler; +import org.openhab.binding.bticinosmarther.internal.handler.SmartherDynamicStateDescriptionProvider; +import org.openhab.binding.bticinosmarther.internal.handler.SmartherModuleHandler; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@code SmartherHandlerFactory} class is responsible for creating things and thing handlers. + * + * @author Fabio Possieri - Initial contribution + */ +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.bticinosmarther") +@NonNullByDefault +public class SmartherHandlerFactory extends BaseThingHandlerFactory { + + private final Logger logger = LoggerFactory.getLogger(SmartherHandlerFactory.class); + + private final OAuthFactory oAuthFactory; + private final SmartherAccountService authService; + private final HttpClient httpClient; + private final CronScheduler cronScheduler; + private final SmartherDynamicStateDescriptionProvider dynamicStateDescriptionProvider; + + @Activate + public SmartherHandlerFactory(@Reference OAuthFactory oAuthFactory, @Reference SmartherAccountService authService, + @Reference HttpClientFactory httpClientFactory, @Reference CronScheduler cronScheduler, + @Reference SmartherDynamicStateDescriptionProvider dynamicStateDescriptionProvider) { + this.oAuthFactory = oAuthFactory; + this.authService = authService; + this.httpClient = httpClientFactory.getCommonHttpClient(); + this.cronScheduler = cronScheduler; + this.dynamicStateDescriptionProvider = dynamicStateDescriptionProvider; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SmartherBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (SmartherBindingConstants.THING_TYPE_BRIDGE.equals(thingTypeUID)) { + final SmartherBridgeHandler handler = new SmartherBridgeHandler((Bridge) thing, oAuthFactory, httpClient); + this.authService.addSmartherAccountHandler(handler); + return handler; + } else if (SmartherBindingConstants.THING_TYPE_MODULE.equals(thingTypeUID)) { + return new SmartherModuleHandler(thing, cronScheduler, dynamicStateDescriptionProvider); + } else { + logger.debug("Unsupported thing {}", thing.getThingTypeUID()); + return null; + } + } + + @Override + protected synchronized void removeHandler(ThingHandler thingHandler) { + if (thingHandler instanceof SmartherBridgeHandler) { + authService.removeSmartherAccountHandler((SmartherBridgeHandler) thingHandler); + } + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/handler/SmartherBridgeHandler.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/handler/SmartherBridgeHandler.java new file mode 100644 index 0000000000000..ff949d4d4b8d5 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/handler/SmartherBridgeHandler.java @@ -0,0 +1,766 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.handler; + +import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.smarthome.config.core.Configuration; +import org.eclipse.smarthome.core.auth.client.oauth2.AccessTokenRefreshListener; +import org.eclipse.smarthome.core.auth.client.oauth2.AccessTokenResponse; +import org.eclipse.smarthome.core.auth.client.oauth2.OAuthClientService; +import org.eclipse.smarthome.core.auth.client.oauth2.OAuthException; +import org.eclipse.smarthome.core.auth.client.oauth2.OAuthFactory; +import org.eclipse.smarthome.core.auth.client.oauth2.OAuthResponseException; +import org.eclipse.smarthome.core.cache.ExpiringCache; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.bticinosmarther.internal.account.SmartherAccountHandler; +import org.openhab.binding.bticinosmarther.internal.account.SmartherNotificationHandler; +import org.openhab.binding.bticinosmarther.internal.api.SmartherApi; +import org.openhab.binding.bticinosmarther.internal.api.dto.Location; +import org.openhab.binding.bticinosmarther.internal.api.dto.Module; +import org.openhab.binding.bticinosmarther.internal.api.dto.ModuleStatus; +import org.openhab.binding.bticinosmarther.internal.api.dto.Notification; +import org.openhab.binding.bticinosmarther.internal.api.dto.Plant; +import org.openhab.binding.bticinosmarther.internal.api.dto.Program; +import org.openhab.binding.bticinosmarther.internal.api.dto.Sender; +import org.openhab.binding.bticinosmarther.internal.api.dto.Subscription; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherAuthorizationException; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException; +import org.openhab.binding.bticinosmarther.internal.config.SmartherBridgeConfiguration; +import org.openhab.binding.bticinosmarther.internal.discovery.SmartherModuleDiscoveryService; +import org.openhab.binding.bticinosmarther.internal.model.BridgeStatus; +import org.openhab.binding.bticinosmarther.internal.model.ModuleSettings; +import org.openhab.binding.bticinosmarther.internal.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@code SmartherBridgeHandler} class is responsible of the handling of a Smarther Bridge thing. + * The Smarther Bridge is used to manage a set of Smarther Chronothermostat Modules registered under the same + * Legrand/Bticino account credentials. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherBridgeHandler extends BaseBridgeHandler + implements SmartherAccountHandler, SmartherNotificationHandler, AccessTokenRefreshListener { + + private static final long POLL_INITIAL_DELAY = 5; + + private final Logger logger = LoggerFactory.getLogger(SmartherBridgeHandler.class); + + private final OAuthFactory oAuthFactory; + private final HttpClient httpClient; + + // Bridge configuration + private SmartherBridgeConfiguration config; + + // Field members assigned in initialize method + private @Nullable Future pollFuture; + private @Nullable OAuthClientService oAuthService; + private @Nullable SmartherApi smartherApi; + private @Nullable ExpiringCache> locationCache; + private @Nullable BridgeStatus bridgeStatus; + + /** + * Constructs a {@code SmartherBridgeHandler} for the given Bridge thing, authorization factory and http client. + * + * @param bridge + * the {@link Bridge} thing to be used + * @param oAuthFactory + * the OAuth2 authorization factory to be used + * @param httpClient + * the http client to be used + */ + public SmartherBridgeHandler(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient) { + super(bridge); + this.oAuthFactory = oAuthFactory; + this.httpClient = httpClient; + this.config = new SmartherBridgeConfiguration(); + } + + @Override + public Collection> getServices() { + return Collections.singleton(SmartherModuleDiscoveryService.class); + } + + // =========================================================================== + // + // Bridge thing lifecycle management methods + // + // =========================================================================== + + @Override + public void initialize() { + logger.debug("Bridge[{}] Initialize handler", thing.getUID()); + + this.config = getConfigAs(SmartherBridgeConfiguration.class); + if (StringUtil.isBlank(config.getSubscriptionKey())) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "The 'Subscription Key' property is not set or empty. If you have an older thing please recreate it."); + return; + } + if (StringUtil.isBlank(config.getClientId())) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "The 'Client Id' property is not set or empty. If you have an older thing please recreate it."); + return; + } + if (StringUtil.isBlank(config.getClientSecret())) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "The 'Client Secret' property is not set or empty. If you have an older thing please recreate it."); + return; + } + + // Initialize OAuth2 authentication support + final OAuthClientService localOAuthService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(), + SMARTHER_API_TOKEN_URL, SMARTHER_AUTHORIZE_URL, config.getClientId(), config.getClientSecret(), + SMARTHER_API_SCOPES, false); + localOAuthService.addAccessTokenRefreshListener(SmartherBridgeHandler.this); + this.oAuthService = localOAuthService; + + // Initialize Smarther Api + final SmartherApi localSmartherApi = new SmartherApi(localOAuthService, config.getSubscriptionKey(), scheduler, + httpClient); + this.smartherApi = localSmartherApi; + + // Initialize locations (plant Ids) local cache + final ExpiringCache> localLocationCache = new ExpiringCache<>( + Duration.ofMinutes(config.getStatusRefreshPeriod()), this::locationCacheAction); + this.locationCache = localLocationCache; + + // Initialize bridge local status + final BridgeStatus localBridgeStatus = new BridgeStatus(); + this.bridgeStatus = localBridgeStatus; + + updateStatus(ThingStatus.UNKNOWN); + + schedulePoll(); + + logger.debug("Bridge[{}] Finished initializing!", thing.getUID()); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + switch (channelUID.getId()) { + case CHANNEL_CONFIG_FETCH_LOCATIONS: + if (command instanceof OnOffType) { + if (OnOffType.ON.equals(command)) { + logger.debug( + "Bridge[{}] Manually triggered channel to remotely fetch the updated client locations list", + thing.getUID()); + expireCache(); + getLocations(); + updateChannelState(CHANNEL_CONFIG_FETCH_LOCATIONS, OnOffType.OFF); + } + return; + } + break; + } + + if (command instanceof RefreshType) { + // Avoid logging wrong command when refresh command is sent + return; + } + + logger.debug("Bridge[{}] Received command {} of wrong type {} on channel {}", thing.getUID(), command, + command.getClass().getTypeName(), channelUID.getId()); + } + + @Override + public void handleRemoval() { + super.handleRemoval(); + stopPoll(true); + } + + @Override + public void dispose() { + logger.debug("Bridge[{}] Dispose handler", thing.getUID()); + final OAuthClientService localOAuthService = this.oAuthService; + if (localOAuthService != null) { + localOAuthService.removeAccessTokenRefreshListener(this); + } + this.oAuthFactory.ungetOAuthService(thing.getUID().getAsString()); + stopPoll(true); + logger.debug("Bridge[{}] Finished disposing!", thing.getUID()); + } + + // =========================================================================== + // + // Bridge data cache management methods + // + // =========================================================================== + + /** + * Returns the available locations to be cached for this Bridge. + * + * @return the available locations to be cached for this Bridge, or {@code null} if the list of available locations + * cannot be retrieved + */ + private @Nullable List locationCacheAction() { + try { + // Retrieve the plants list from the API Gateway + final List plants = getPlants(); + + List locations; + if (config.isUseNotifications()) { + // Retrieve the subscriptions list from the API Gateway + final List subscriptions = getSubscriptions(); + + // Enrich the notifications list with externally registered subscriptions + updateNotifications(subscriptions); + + // Get the notifications list from bridge config + final List notifications = config.getNotifications(); + + locations = plants.stream().map(p -> Location.fromPlant(p, subscriptions.stream() + .filter(s -> s.getPlantId().equals(p.getId()) && notifications.contains(s.getSubscriptionId())) + .findFirst())).collect(Collectors.toList()); + } else { + locations = plants.stream().map(p -> Location.fromPlant(p)).collect(Collectors.toList()); + } + logger.debug("Bridge[{}] Available locations: {}", thing.getUID(), locations); + + return locations; + + } catch (SmartherGatewayException e) { + logger.warn("Bridge[{}] Cannot retrieve available locations: {}", thing.getUID(), e.getMessage()); + return null; + } + } + + /** + * Updates this Bridge local notifications list with externally registered subscriptions. + * + * @param subscriptions + * the externally registered subscriptions to be added to the local notifications list + */ + private void updateNotifications(List subscriptions) { + // Get the notifications list from bridge config + List notifications = config.getNotifications(); + + for (Subscription s : subscriptions) { + if (s.getEndpointUrl().equalsIgnoreCase(config.getNotificationUrl()) + && !notifications.contains(s.getSubscriptionId())) { + // Add the external subscription to notifications list + notifications = config.addNotification(s.getSubscriptionId()); + + // Save the updated notifications list back to bridge config + Configuration configuration = editConfiguration(); + configuration.put(PROPERTY_NOTIFICATIONS, notifications); + updateConfiguration(configuration); + } + } + } + + /** + * Sets all the cache to "expired" for this Bridge. + */ + private void expireCache() { + logger.debug("Bridge[{}] Invalidating location cache", thing.getUID()); + final ExpiringCache> localLocationCache = this.locationCache; + if (localLocationCache != null) { + localLocationCache.invalidateValue(); + } + } + + // =========================================================================== + // + // Bridge status polling mechanism methods + // + // =========================================================================== + + /** + * Starts a new scheduler to periodically poll and update this Bridge status. + */ + private void schedulePoll() { + stopPoll(false); + + // Schedule poll to start after POLL_INITIAL_DELAY sec and run periodically based on status refresh period + final Future localPollFuture = scheduler.scheduleWithFixedDelay(this::poll, POLL_INITIAL_DELAY, + config.getStatusRefreshPeriod() * 60, TimeUnit.SECONDS); + this.pollFuture = localPollFuture; + + logger.debug("Bridge[{}] Scheduled poll for {} sec out, then every {} min", thing.getUID(), POLL_INITIAL_DELAY, + config.getStatusRefreshPeriod()); + } + + /** + * Cancels all running poll schedulers. + * + * @param mayInterruptIfRunning + * {@code true} if the thread executing this task should be interrupted, {@code false} if the in-progress + * tasks are allowed to complete + */ + private synchronized void stopPoll(boolean mayInterruptIfRunning) { + final Future localPollFuture = this.pollFuture; + if (localPollFuture != null) { + if (!localPollFuture.isCancelled()) { + localPollFuture.cancel(mayInterruptIfRunning); + } + this.pollFuture = null; + } + } + + /** + * Polls to update this Bridge status, calling the Smarther API to refresh its plants list. + * + * @return {@code true} if the method completes without errors, {@code false} otherwise + */ + private synchronized boolean poll() { + try { + onAccessTokenResponse(getAccessTokenResponse()); + + expireCache(); + getLocations(); + + updateStatus(ThingStatus.ONLINE); + return true; + } catch (SmartherAuthorizationException e) { + logger.warn("Bridge[{}] Authorization error during polling: {}", thing.getUID(), e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } catch (RuntimeException e) { + // All other exceptions apart from Authorization and Gateway issues + logger.warn("Bridge[{}] Unexpected error during polling, please report if this keeps occurring: ", + thing.getUID(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage()); + } + schedulePoll(); + return false; + } + + @Override + public void onAccessTokenResponse(@Nullable AccessTokenResponse tokenResponse) { + logger.trace("Bridge[{}] Got access token: {}", thing.getUID(), + (tokenResponse != null) ? tokenResponse.getAccessToken() : "none"); + } + + // =========================================================================== + // + // Bridge convenience methods + // + // =========================================================================== + + /** + * Convenience method to get this Bridge configuration. + * + * @return a {@link SmartherBridgeConfiguration} object containing the Bridge configuration + */ + public SmartherBridgeConfiguration getSmartherBridgeConfig() { + return config; + } + + /** + * Convenience method to get the access token from Smarther API authorization layer. + * + * @return the autorization access token, may be {@code null} + * + * @throws {@link SmartherAuthorizationException} + * in case of authorization issues with the Smarther API + */ + private @Nullable AccessTokenResponse getAccessTokenResponse() throws SmartherAuthorizationException { + try { + final OAuthClientService localOAuthService = this.oAuthService; + if (localOAuthService != null) { + return localOAuthService.getAccessTokenResponse(); + } + return null; + } catch (OAuthException | IOException | OAuthResponseException | RuntimeException e) { + throw new SmartherAuthorizationException(e.getMessage()); + } + } + + /** + * Convenience method to update the given Channel state "only" if the Channel is linked. + * + * @param channelId + * the identifier of the Channel to be updated + * @param state + * the new state to be applied to the given Channel + */ + private void updateChannelState(String channelId, State state) { + final Channel channel = thing.getChannel(channelId); + + if (channel != null && isLinked(channel.getUID())) { + updateState(channel.getUID(), state); + } + } + + /** + * Convenience method to update the Smarther API calls counter for this Bridge. + */ + private void updateApiCallsCounter() { + final BridgeStatus localBridgeStatus = this.bridgeStatus; + if (localBridgeStatus != null) { + updateChannelState(CHANNEL_STATUS_API_CALLS_HANDLED, + new DecimalType(localBridgeStatus.incrementApiCallsHandled())); + } + } + + /** + * Convenience method to check and get the Smarther API instance for this Bridge. + * + * @return the Smarther API instance + * + * @throws {@link SmartherGatewayException} + * in case the Smarther API instance is {@code null} + */ + private SmartherApi getSmartherApi() throws SmartherGatewayException { + final SmartherApi localSmartherApi = this.smartherApi; + if (localSmartherApi == null) { + throw new SmartherGatewayException("Smarther API instance is null"); + } + return localSmartherApi; + } + + // =========================================================================== + // + // Implementation of the SmartherAccountHandler interface + // + // =========================================================================== + + @Override + public ThingUID getUID() { + return thing.getUID(); + } + + @Override + public String getLabel() { + return StringUtil.defaultString(thing.getLabel()); + } + + @Override + public List getLocations() { + final ExpiringCache> localLocationCache = this.locationCache; + final List locations = (localLocationCache != null) ? localLocationCache.getValue() : null; + return (locations != null) ? locations : Collections.emptyList(); + } + + @Override + public boolean hasLocation(String plantId) { + final ExpiringCache> localLocationCache = this.locationCache; + final List locations = (localLocationCache != null) ? localLocationCache.getValue() : null; + return (locations != null) ? locations.stream().anyMatch(l -> l.getPlantId().equals(plantId)) : false; + } + + @Override + public List getPlants() throws SmartherGatewayException { + updateApiCallsCounter(); + return getSmartherApi().getPlants(); + } + + @Override + public List getSubscriptions() throws SmartherGatewayException { + updateApiCallsCounter(); + return getSmartherApi().getSubscriptions(); + } + + @Override + public String subscribePlant(String plantId, String notificationUrl) throws SmartherGatewayException { + updateApiCallsCounter(); + return getSmartherApi().subscribePlant(plantId, notificationUrl); + } + + @Override + public void unsubscribePlant(String plantId, String subscriptionId) throws SmartherGatewayException { + updateApiCallsCounter(); + getSmartherApi().unsubscribePlant(plantId, subscriptionId); + } + + @Override + public List getLocationModules(Location location) { + try { + updateApiCallsCounter(); + return getSmartherApi().getPlantModules(location.getPlantId()); + } catch (SmartherGatewayException e) { + return new ArrayList<>(); + } + } + + @Override + public ModuleStatus getModuleStatus(String plantId, String moduleId) throws SmartherGatewayException { + updateApiCallsCounter(); + return getSmartherApi().getModuleStatus(plantId, moduleId); + } + + @Override + public boolean setModuleStatus(ModuleSettings moduleSettings) throws SmartherGatewayException { + updateApiCallsCounter(); + return getSmartherApi().setModuleStatus(moduleSettings); + } + + @Override + public List getModulePrograms(String plantId, String moduleId) throws SmartherGatewayException { + updateApiCallsCounter(); + return getSmartherApi().getModulePrograms(plantId, moduleId); + } + + @Override + public boolean isAuthorized() { + try { + final AccessTokenResponse tokenResponse = getAccessTokenResponse(); + onAccessTokenResponse(tokenResponse); + + return (tokenResponse != null && tokenResponse.getAccessToken() != null + && tokenResponse.getRefreshToken() != null); + } catch (SmartherAuthorizationException e) { + return false; + } + } + + @Override + public boolean isOnline() { + return (thing.getStatus() == ThingStatus.ONLINE); + } + + @Override + public String authorize(String redirectUrl, String reqCode, String notificationUrl) + throws SmartherGatewayException { + try { + logger.debug("Bridge[{}] Call API gateway to get access token. RedirectUri: {}", thing.getUID(), + redirectUrl); + + final OAuthClientService localOAuthService = this.oAuthService; + if (localOAuthService == null) { + throw new SmartherAuthorizationException("Authorization service is null"); + } + + // OAuth2 call to get access token from received authorization code + localOAuthService.getAccessTokenResponseByAuthorizationCode(reqCode, redirectUrl); + + // Store the notification URL in bridge configuration + Configuration configuration = editConfiguration(); + configuration.put(PROPERTY_NOTIFICATION_URL, notificationUrl); + updateConfiguration(configuration); + config.setNotificationUrl(notificationUrl); + logger.debug("Bridge[{}] Store notification URL: {}", thing.getUID(), notificationUrl); + + // Reschedule the polling thread + schedulePoll(); + + return config.getClientId(); + } catch (OAuthResponseException e) { + throw new SmartherAuthorizationException(e.toString(), e); + } catch (OAuthException | IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + throw new SmartherGatewayException(e.getMessage(), e); + } + } + + @Override + public boolean equalsThingUID(String thingUID) { + return thing.getUID().getAsString().equals(thingUID); + } + + @Override + public String formatAuthorizationUrl(String redirectUri) { + try { + final OAuthClientService localOAuthService = this.oAuthService; + if (localOAuthService != null) { + return localOAuthService.getAuthorizationUrl(redirectUri, null, thing.getUID().getAsString()); + } + } catch (OAuthException e) { + logger.warn("Bridge[{}] Error constructing AuthorizationUrl: {}", thing.getUID(), e.getMessage()); + } + return ""; + } + + // =========================================================================== + // + // Implementation of the SmartherNotificationHandler interface + // + // =========================================================================== + + @Override + public boolean useNotifications() { + return config.isUseNotifications(); + } + + @Override + public synchronized void registerNotification(String plantId) throws SmartherGatewayException { + if (!config.isUseNotifications()) { + return; + } + + final ExpiringCache> localLocationCache = this.locationCache; + if (localLocationCache != null) { + List locations = localLocationCache.getValue(); + if (locations != null) { + final Optional maybeLocation = locations.stream().filter(l -> l.getPlantId().equals(plantId)) + .findFirst(); + if (maybeLocation.isPresent()) { + Location location = maybeLocation.get(); + if (!location.hasSubscription()) { + // Validate notification Url (must be non-null and https) + final String notificationUrl = config.getNotificationUrl(); + if (isValidNotificationUrl(notificationUrl)) { + // Call gateway to register plant subscription + String subscriptionId = subscribePlant(plantId, config.getNotificationUrl()); + logger.debug("Bridge[{}] Notification registered: [plantId={}, subscriptionId={}]", + thing.getUID(), plantId, subscriptionId); + + // Add the new subscription to notifications list + List notifications = config.addNotification(subscriptionId); + + // Save the updated notifications list back to bridge config + Configuration configuration = editConfiguration(); + configuration.put(PROPERTY_NOTIFICATIONS, notifications); + updateConfiguration(configuration); + + // Update the local locationCache with the added data + locations.stream().forEach(l -> { + if (l.getPlantId().equals(plantId)) { + l.setSubscription(subscriptionId, config.getNotificationUrl()); + } + }); + localLocationCache.putValue(locations); + } else { + logger.warn( + "Bridge[{}] Invalid notification Url [{}]: must be non-null, public https address", + thing.getUID(), notificationUrl); + } + } + } + } + } + } + + @Override + public void handleNotification(Notification notification) { + final Sender sender = notification.getSender(); + if (sender != null) { + final BridgeStatus localBridgeStatus = this.bridgeStatus; + if (localBridgeStatus != null) { + logger.debug("Bridge[{}] Notification received: [id={}]", thing.getUID(), notification.getId()); + updateChannelState(CHANNEL_STATUS_NOTIFS_RECEIVED, + new DecimalType(localBridgeStatus.incrementNotificationsReceived())); + + final String plantId = sender.getPlant().getId(); + final String moduleId = sender.getPlant().getModule().getId(); + Optional maybeModuleHandler = getThing().getThings().stream() + .map(t -> (SmartherModuleHandler) t.getHandler()).filter(h -> h.isLinkedTo(plantId, moduleId)) + .findFirst(); + + if (config.isUseNotifications() && maybeModuleHandler.isPresent()) { + maybeModuleHandler.get().handleNotification(notification); + } else { + logger.debug("Bridge[{}] Notification rejected: no module handler available", thing.getUID()); + updateChannelState(CHANNEL_STATUS_NOTIFS_REJECTED, + new DecimalType(localBridgeStatus.incrementNotificationsRejected())); + } + } + } + } + + @Override + public synchronized void unregisterNotification(String plantId) throws SmartherGatewayException { + if (!config.isUseNotifications()) { + return; + } + + final ExpiringCache> localLocationCache = this.locationCache; + if (localLocationCache != null) { + List locations = localLocationCache.getValue(); + + final long remainingModules = getThing().getThings().stream() + .map(t -> (SmartherModuleHandler) t.getHandler()).filter(h -> h.getPlantId().equals(plantId)) + .count(); + + if (locations != null && remainingModules == 0) { + final Optional maybeLocation = locations.stream().filter(l -> l.getPlantId().equals(plantId)) + .findFirst(); + if (maybeLocation.isPresent()) { + Location location = maybeLocation.get(); + final String subscriptionId = location.getSubscriptionId(); + if (location.hasSubscription() && (subscriptionId != null)) { + // Call gateway to unregister plant subscription + unsubscribePlant(plantId, subscriptionId); + logger.debug("Bridge[{}] Notification unregistered: [plantId={}, subscriptionId={}]", + thing.getUID(), plantId, subscriptionId); + + // Remove the subscription from notifications list + List notifications = config.removeNotification(subscriptionId); + + // Save the updated notifications list back to bridge config + Configuration configuration = editConfiguration(); + configuration.put(PROPERTY_NOTIFICATIONS, notifications); + updateConfiguration(configuration); + + // Update the local locationCache with the removed data + locations.stream().forEach(l -> { + if (l.getPlantId().equals(plantId)) { + l.unsetSubscription(); + } + }); + localLocationCache.putValue(locations); + } + } + } + } + } + + /** + * Checks if the passed string is a formally valid Notification Url (non-null, public https address). + * + * @param str + * the string to check + * + * @return {@code true} if the given string is a formally valid Notification Url, {@code false} otherwise + */ + private boolean isValidNotificationUrl(@Nullable String str) { + try { + if (str != null) { + URI maybeValidNotificationUrl = new URI(str); + if (HTTPS_SCHEMA.equals(maybeValidNotificationUrl.getScheme())) { + InetAddress address = InetAddress.getByName(maybeValidNotificationUrl.getHost()); + if (!address.isLoopbackAddress() && !address.isSiteLocalAddress()) { + return true; + } + } + } + return false; + } catch (URISyntaxException | UnknownHostException e) { + return false; + } + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/handler/SmartherDynamicStateDescriptionProvider.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/handler/SmartherDynamicStateDescriptionProvider.java new file mode 100644 index 0000000000000..c673c7bc11c6a --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/handler/SmartherDynamicStateDescriptionProvider.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.handler; + +import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.DTF_DATE; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.binding.BaseDynamicStateDescriptionProvider; +import org.eclipse.smarthome.core.thing.type.DynamicStateDescriptionProvider; +import org.eclipse.smarthome.core.types.StateOption; +import org.openhab.binding.bticinosmarther.internal.api.dto.Program; +import org.openhab.binding.bticinosmarther.internal.util.DateUtil; +import org.osgi.service.component.annotations.Component; + +/** + * Dynamically create the users list of programs and setting dates. + * + * @author Fabio Possieri - Initial contribution + */ +@Component(service = { DynamicStateDescriptionProvider.class, SmartherDynamicStateDescriptionProvider.class }) +@NonNullByDefault +public class SmartherDynamicStateDescriptionProvider extends BaseDynamicStateDescriptionProvider { + + private static final String LABEL_FOREVER = "Forever"; + private static final String LABEL_TODAY = "Today"; + private static final String LABEL_TOMORROW = "Tomorrow"; + + public void setEndDates(ChannelUID channelUID, int maxEndDays) { + List endDates = new ArrayList<>(); + + endDates.add(new StateOption("", LABEL_FOREVER)); + + final LocalDateTime today = LocalDate.now().atStartOfDay(); + + endDates.add(new StateOption(DateUtil.format(today, DTF_DATE), LABEL_TODAY)); + if (maxEndDays > 1) { + endDates.add(new StateOption(DateUtil.format(today.plusDays(1), DTF_DATE), LABEL_TOMORROW)); + for (int i = 2; i < maxEndDays; i++) { + final String newDate = DateUtil.format(today.plusDays(i), DTF_DATE); + endDates.add(new StateOption(newDate, newDate)); + } + } + + setStateOptions(channelUID, endDates); + } + + public void setPrograms(ChannelUID channelUID, @Nullable List programs) { + if (programs != null) { + setStateOptions(channelUID, + programs.stream() + .map(program -> new StateOption(String.valueOf(program.getNumber()), program.getName())) + .collect(Collectors.toList())); + } + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/handler/SmartherModuleHandler.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/handler/SmartherModuleHandler.java new file mode 100644 index 0000000000000..465097faffb86 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/handler/SmartherModuleHandler.java @@ -0,0 +1,735 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.handler; + +import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.cache.ExpiringCache; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.eclipse.smarthome.core.scheduler.CronScheduler; +import org.eclipse.smarthome.core.scheduler.ScheduledCompletableFuture; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.ThingStatusInfo; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.bticinosmarther.internal.api.dto.Chronothermostat; +import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.BoostTime; +import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.Mode; +import org.openhab.binding.bticinosmarther.internal.api.dto.ModuleStatus; +import org.openhab.binding.bticinosmarther.internal.api.dto.Notification; +import org.openhab.binding.bticinosmarther.internal.api.dto.Program; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherIllegalPropertyValueException; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherSubscriptionAlreadyExistsException; +import org.openhab.binding.bticinosmarther.internal.config.SmartherModuleConfiguration; +import org.openhab.binding.bticinosmarther.internal.model.ModuleSettings; +import org.openhab.binding.bticinosmarther.internal.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@code SmartherModuleHandler} class is responsible of a single Smarther Chronothermostat, handling the commands + * that are sent to one of its channels. + * Each Smarther Chronothermostat communicates with the Smarther API via its assigned {@code SmartherBridgeHandler}. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class SmartherModuleHandler extends BaseThingHandler { + + private static final String DAILY_MIDNIGHT = "1 0 0 * * ? *"; + private static final long POLL_INITIAL_DELAY = 5; + + private final Logger logger = LoggerFactory.getLogger(SmartherModuleHandler.class); + + private final CronScheduler cronScheduler; + private final SmartherDynamicStateDescriptionProvider dynamicStateDescriptionProvider; + private final ChannelUID programChannelUID; + private final ChannelUID endDateChannelUID; + + // Module configuration + private SmartherModuleConfiguration config; + + // Field members assigned in initialize method + private @Nullable ScheduledCompletableFuture jobFuture; + private @Nullable Future pollFuture; + private @Nullable SmartherBridgeHandler bridgeHandler; + private @Nullable ExpiringCache> programCache; + private @Nullable ModuleSettings moduleSettings; + + // Chronothermostat local status + private @Nullable Chronothermostat chronothermostat; + + /** + * Constructs a {@code SmartherModuleHandler} for the given thing, scheduler and dynamic state description provider. + * + * @param thing + * the {@link Thing} thing to be used + * @param scheduler + * the {@link CronScheduler} periodic job scheduler to be used + * @param provider + * the {@link SmartherDynamicStateDescriptionProvider} dynamic state description provider to be used + */ + public SmartherModuleHandler(Thing thing, CronScheduler scheduler, + SmartherDynamicStateDescriptionProvider provider) { + super(thing); + this.cronScheduler = scheduler; + this.dynamicStateDescriptionProvider = provider; + this.programChannelUID = new ChannelUID(thing.getUID(), CHANNEL_SETTINGS_PROGRAM); + this.endDateChannelUID = new ChannelUID(thing.getUID(), CHANNEL_SETTINGS_ENDDATE); + this.config = new SmartherModuleConfiguration(); + } + + // =========================================================================== + // + // Chronothermostat thing lifecycle management methods + // + // =========================================================================== + + @Override + public void initialize() { + logger.debug("Module[{}] Initialize handler", thing.getUID()); + + final Bridge localBridge = getBridge(); + if (localBridge == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED); + return; + } + + final SmartherBridgeHandler localBridgeHandler = (SmartherBridgeHandler) localBridge.getHandler(); + this.bridgeHandler = localBridgeHandler; + if (localBridgeHandler == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format( + "Missing configuration from the Smarther Bridge (UID:%s). Fix configuration or report if this problem remains.", + localBridge.getBridgeUID())); + return; + } + + this.config = getConfigAs(SmartherModuleConfiguration.class); + if (StringUtil.isBlank(config.getPlantId())) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "The 'Plant Id' property is not set or empty. If you have an older thing please recreate it."); + return; + } + if (StringUtil.isBlank(config.getModuleId())) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "The 'Module Id' property is not set or empty. If you have an older thing please recreate it."); + return; + } + if (config.getProgramsRefreshPeriod() <= 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "The 'Programs Refresh Period' must be > 0. If you have an older thing please recreate it."); + return; + } + if (config.getStatusRefreshPeriod() <= 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "The 'Module Status Refresh Period' must be > 0. If you have an older thing please recreate it."); + return; + } + + // Initialize automatic mode programs local cache + final ExpiringCache> localProgramCache = new ExpiringCache<>( + Duration.ofHours(config.getProgramsRefreshPeriod()), this::programCacheAction); + this.programCache = localProgramCache; + + // Initialize module local settings + final ModuleSettings localModuleSettings = new ModuleSettings(config.getPlantId(), config.getModuleId()); + this.moduleSettings = localModuleSettings; + + updateStatus(ThingStatus.UNKNOWN); + + scheduleJob(); + schedulePoll(); + + logger.debug("Module[{}] Finished initializing!", thing.getUID()); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + try { + handleCommandInternal(channelUID, command); + updateModuleStatus(); + } catch (SmartherIllegalPropertyValueException e) { + logger.warn("Module[{}] Received command {} with illegal value {} on channel {}", thing.getUID(), command, + e.getMessage(), channelUID.getId()); + } catch (SmartherGatewayException e) { + // catch exceptions and handle it in your binding + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + + /** + * Handles the command sent to a given Channel of this Chronothermostat. + * + * @param channelUID + * the identifier of the Channel + * @param command + * the command sent to the given Channel + * + * @throws {@link SmartherIllegalPropertyValueException} + * if the command contains an illegal value that cannot be mapped to any valid enum value + * @throws {@link SmartherGatewayException} + * in case of communication issues with the Smarther API + */ + private void handleCommandInternal(ChannelUID channelUID, Command command) + throws SmartherIllegalPropertyValueException, SmartherGatewayException { + final ModuleSettings localModuleSettings = this.moduleSettings; + if (localModuleSettings == null) { + return; + } + + switch (channelUID.getId()) { + case CHANNEL_SETTINGS_MODE: + if (command instanceof StringType) { + localModuleSettings.setMode(Mode.fromValue(command.toString())); + return; + } + break; + case CHANNEL_SETTINGS_TEMPERATURE: + if (changeTemperature(command, localModuleSettings)) { + return; + } + break; + case CHANNEL_SETTINGS_PROGRAM: + if (command instanceof DecimalType) { + localModuleSettings.setProgram(((DecimalType) command).intValue()); + return; + } + break; + case CHANNEL_SETTINGS_BOOSTTIME: + if (command instanceof DecimalType) { + localModuleSettings.setBoostTime(BoostTime.fromValue(((DecimalType) command).intValue())); + return; + } + break; + case CHANNEL_SETTINGS_ENDDATE: + if (command instanceof StringType) { + localModuleSettings.setEndDate(command.toString()); + return; + } + break; + case CHANNEL_SETTINGS_ENDHOUR: + if (changeTimeHour(command, localModuleSettings)) { + return; + } + break; + case CHANNEL_SETTINGS_ENDMINUTE: + if (changeTimeMinute(command, localModuleSettings)) { + return; + } + break; + case CHANNEL_SETTINGS_POWER: + if (command instanceof OnOffType) { + if (OnOffType.ON.equals(command)) { + // Apply module settings to the remote module + if (getBridgeHandler().setModuleStatus(localModuleSettings)) { + // Change applied, update module status + logger.debug("Module[{}] New settings applied!", thing.getUID()); + } + updateChannelState(CHANNEL_SETTINGS_POWER, OnOffType.OFF); + } + return; + } + break; + case CHANNEL_CONFIG_FETCH_PROGRAMS: + if (command instanceof OnOffType) { + if (OnOffType.ON.equals(command)) { + logger.debug( + "Module[{}] Manually triggered channel to remotely fetch the updated programs list", + thing.getUID()); + expireCache(); + refreshProgramsList(); + updateChannelState(CHANNEL_CONFIG_FETCH_PROGRAMS, OnOffType.OFF); + } + return; + } + break; + } + + if (command instanceof RefreshType) { + // Avoid logging wrong command when refresh command is sent + return; + } + + logger.debug("Module[{}] Received command {} of wrong type {} on channel {}", thing.getUID(), command, + command.getClass().getTypeName(), channelUID.getId()); + } + + /** + * Changes the "temperature" in module settings, based on the received Command. + * The new value is checked against the temperature limits allowed by the device. + * + * @param command + * the command received on temperature Channel + * + * @return {@code true} if the change succeeded, {@code false} otherwise + */ + private boolean changeTemperature(Command command, final ModuleSettings settings) { + if (!(command instanceof QuantityType)) { + return false; + } + + QuantityType quantity = (QuantityType) command; + QuantityType newMeasure = quantity.toUnit(SIUnits.CELSIUS); + + // Check remote device temperature limits + if (newMeasure != null && newMeasure.doubleValue() >= 7.1 && newMeasure.doubleValue() <= 40.0) { + // Only tenth degree increments are allowed + double newTemperature = Math.round(newMeasure.doubleValue() * 10) / 10.0; + + settings.setSetPointTemperature(QuantityType.valueOf(newTemperature, SIUnits.CELSIUS)); + return true; + } + return false; + } + + /** + * Changes the "end hour" for manual mode in module settings, based on the received Command. + * The new value is checked against the 24-hours clock allowed range. + * + * @param command + * the command received on end hour Channel + * + * @return {@code true} if the change succeeded, {@code false} otherwise + */ + private boolean changeTimeHour(Command command, final ModuleSettings settings) { + if (command instanceof DecimalType) { + int endHour = ((DecimalType) command).intValue(); + if (endHour >= 0 && endHour <= 23) { + settings.setEndHour(endHour); + return true; + } + } + return false; + } + + /** + * Changes the "end minute" for manual mode in module settings, based on the received Command. + * The new value is modified to match a 15 min step increment. + * + * @param command + * the command received on end minute Channel + * + * @return {@code true} if the change succeeded, {@code false} otherwise + */ + private boolean changeTimeMinute(Command command, final ModuleSettings settings) { + if (command instanceof DecimalType) { + int endMinute = ((DecimalType) command).intValue(); + if (endMinute >= 0 && endMinute <= 59) { + // Only 15 min increments are allowed + endMinute = Math.round(endMinute / 15) * 15; + settings.setEndMinute(endMinute); + return true; + } + } + return false; + } + + /** + * Handles the notification dispatched to this Chronothermostat from the reference Smarther Bridge. + * + * @param notification + * the notification to handle + */ + public void handleNotification(Notification notification) { + try { + final Chronothermostat notificationChrono = notification.getChronothermostat(); + if (notificationChrono != null) { + this.chronothermostat = notificationChrono; + if (config.isSettingsAutoupdate()) { + final ModuleSettings localModuleSettings = this.moduleSettings; + if (localModuleSettings != null) { + localModuleSettings.updateFromChronothermostat(notificationChrono); + } + } + logger.debug("Module[{}] Handle notification: [{}]", thing.getUID(), this.chronothermostat); + updateModuleStatus(); + } + } catch (SmartherIllegalPropertyValueException e) { + logger.warn("Module[{}] Notification has illegal value: [{}]", thing.getUID(), e.getMessage()); + } + } + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) { + // Put module offline when the parent bridge goes offline + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Smarther Bridge Offline"); + logger.debug("Module[{}] Bridge switched {}", thing.getUID(), bridgeStatusInfo.getStatus()); + } else { + // Update the module status when the parent bridge return online + logger.debug("Module[{}] Bridge is back ONLINE", thing.getUID()); + // Restart polling to collect module data + schedulePoll(); + } + } + + @Override + public void handleRemoval() { + super.handleRemoval(); + stopPoll(true); + stopJob(true); + } + + @Override + public void dispose() { + logger.debug("Module[{}] Dispose handler", thing.getUID()); + stopPoll(true); + stopJob(true); + try { + getBridgeHandler().unregisterNotification(config.getPlantId()); + } catch (SmartherGatewayException e) { + logger.warn("Module[{}] API Gateway error during disposing: {}", thing.getUID(), e.getMessage()); + } + logger.debug("Module[{}] Finished disposing!", thing.getUID()); + } + + // =========================================================================== + // + // Chronothermostat data cache management methods + // + // =========================================================================== + + /** + * Returns the available automatic mode programs to be cached for this Chronothermostat. + * + * @return the available programs to be cached for this Chronothermostat, or {@code null} if the list of available + * programs cannot be retrieved + */ + private @Nullable List programCacheAction() { + try { + final List programs = getBridgeHandler().getModulePrograms(config.getPlantId(), + config.getModuleId()); + logger.debug("Module[{}] Available programs: {}", thing.getUID(), programs); + + return programs; + + } catch (SmartherGatewayException e) { + logger.warn("Module[{}] Cannot retrieve available programs: {}", thing.getUID(), e.getMessage()); + return null; + } + } + + /** + * Sets all the cache to "expired" for this Chronothermostat. + */ + private void expireCache() { + logger.debug("Module[{}] Invalidating program cache", thing.getUID()); + final ExpiringCache> localProgramCache = this.programCache; + if (localProgramCache != null) { + localProgramCache.invalidateValue(); + } + } + + // =========================================================================== + // + // Chronothermostat job scheduler methods + // + // =========================================================================== + + /** + * Starts a new cron scheduler to execute the internal recurring jobs. + */ + private synchronized void scheduleJob() { + stopJob(false); + + // Schedule daily job to start daily, at midnight + final ScheduledCompletableFuture localJobFuture = cronScheduler.schedule(this::dailyJob, DAILY_MIDNIGHT); + this.jobFuture = localJobFuture; + + logger.debug("Module[{}] Scheduled recurring job {} to start at midnight", thing.getUID(), + Integer.toHexString(localJobFuture.hashCode())); + + // Execute daily job immediately at startup + this.dailyJob(); + } + + /** + * Cancels all running jobs. + * + * @param mayInterruptIfRunning + * {@code true} if the thread executing this task should be interrupted, {@code false} if the in-progress + * tasks are allowed to complete + */ + private synchronized void stopJob(boolean mayInterruptIfRunning) { + final ScheduledCompletableFuture localJobFuture = this.jobFuture; + if (localJobFuture != null) { + if (!localJobFuture.isCancelled()) { + localJobFuture.cancel(mayInterruptIfRunning); + } + this.jobFuture = null; + } + } + + /** + * Action to be executed by the daily job: refresh the end dates list for "manual" mode. + */ + private void dailyJob() { + logger.debug("Module[{}] Daily job, refreshing the end dates list for \"manual\" mode", thing.getUID()); + // Refresh the end dates list for "manual" mode + dynamicStateDescriptionProvider.setEndDates(endDateChannelUID, config.getNumberOfEndDays()); + // If expired, update EndDate in module settings + final ModuleSettings localModuleSettings = this.moduleSettings; + if (localModuleSettings != null && localModuleSettings.isEndDateExpired()) { + localModuleSettings.refreshEndDate(); + updateChannelState(CHANNEL_SETTINGS_ENDDATE, new StringType(localModuleSettings.getEndDate())); + } + } + + // =========================================================================== + // + // Chronothermostat status polling mechanism methods + // + // =========================================================================== + + /** + * Starts a new scheduler to periodically poll and update this Chronothermostat status. + */ + private void schedulePoll() { + stopPoll(false); + + // Schedule poll to start after POLL_INITIAL_DELAY sec and run periodically based on status refresh period + final Future localPollFuture = scheduler.scheduleWithFixedDelay(this::poll, POLL_INITIAL_DELAY, + config.getStatusRefreshPeriod() * 60, TimeUnit.SECONDS); + this.pollFuture = localPollFuture; + + logger.debug("Module[{}] Scheduled poll for {} sec out, then every {} min", thing.getUID(), POLL_INITIAL_DELAY, + config.getStatusRefreshPeriod()); + } + + /** + * Cancels all running poll schedulers. + * + * @param mayInterruptIfRunning + * {@code true} if the thread executing this task should be interrupted, {@code false} if the in-progress + * tasks are allowed to complete + */ + private synchronized void stopPoll(boolean mayInterruptIfRunning) { + final Future localPollFuture = this.pollFuture; + if (localPollFuture != null) { + if (!localPollFuture.isCancelled()) { + localPollFuture.cancel(mayInterruptIfRunning); + } + this.pollFuture = null; + } + } + + /** + * Polls to update this Chronothermostat status. + * + * @return {@code true} if the method completes without errors, {@code false} otherwise + */ + private synchronized boolean poll() { + try { + final Bridge bridge = getBridge(); + if (bridge != null) { + final ThingStatusInfo bridgeStatusInfo = bridge.getStatusInfo(); + if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) { + ModuleStatus moduleStatus = getBridgeHandler().getModuleStatus(config.getPlantId(), + config.getModuleId()); + + final Chronothermostat statusChrono = moduleStatus.toChronothermostat(); + if (statusChrono != null) { + if ((this.chronothermostat == null) || config.isSettingsAutoupdate()) { + final ModuleSettings localModuleSettings = this.moduleSettings; + if (localModuleSettings != null) { + localModuleSettings.updateFromChronothermostat(statusChrono); + } + } + this.chronothermostat = statusChrono; + logger.debug("Module[{}] Status: [{}]", thing.getUID(), this.chronothermostat); + } else { + throw new SmartherGatewayException("No chronothermostat data found"); + } + + // Refresh the programs list for "automatic" mode + refreshProgramsList(); + + updateModuleStatus(); + + getBridgeHandler().registerNotification(config.getPlantId()); + + // Everything is ok > set the Thing state to Online + updateStatus(ThingStatus.ONLINE); + return true; + } else if (thing.getStatus() != ThingStatus.OFFLINE) { + logger.debug("Module[{}] Switched {} as Bridge is not online", thing.getUID(), + bridgeStatusInfo.getStatus()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Smarther Bridge Offline"); + } + } + return false; + } catch (SmartherIllegalPropertyValueException e) { + logger.debug("Module[{}] Illegal property value error during polling: {}", thing.getUID(), e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage()); + } catch (SmartherSubscriptionAlreadyExistsException e) { + logger.debug("Module[{}] Subscription error during polling: {}", thing.getUID(), e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage()); + } catch (SmartherGatewayException e) { + logger.warn("Module[{}] API Gateway error during polling: {}", thing.getUID(), e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (RuntimeException e) { + // All other exceptions apart from Subscription and Gateway issues + logger.warn("Module[{}] Unexpected error during polling, please report if this keeps occurring: ", + thing.getUID(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage()); + } + schedulePoll(); + return false; + } + + // =========================================================================== + // + // Chronothermostat convenience methods + // + // =========================================================================== + + /** + * Convenience method to check and get the Smarther Bridge handler instance for this Module. + * + * @return the Smarther Bridge handler instance + * + * @throws {@link SmartherGatewayException} + * in case the Smarther Bridge handler instance is {@code null} + */ + private SmartherBridgeHandler getBridgeHandler() throws SmartherGatewayException { + final SmartherBridgeHandler localBridgeHandler = this.bridgeHandler; + if (localBridgeHandler == null) { + throw new SmartherGatewayException("Smarther Bridge handler instance is null"); + } + return localBridgeHandler; + } + + /** + * Returns this Chronothermostat plant identifier + * + * @return a string containing the plant identifier + */ + public String getPlantId() { + return config.getPlantId(); + } + + /** + * Returns this Chronothermostat module identifier + * + * @return a string containing the module identifier + */ + public String getModuleId() { + return config.getModuleId(); + } + + /** + * Checks whether this Chronothermostat matches with the given plant and module identifiers. + * + * @param plantId + * the plant identifier to match to + * @param moduleId + * the module identifier to match to + * + * @return {@code true} if the Chronothermostat matches the given plant and module identifiers, {@code false} + * otherwise + */ + public boolean isLinkedTo(String plantId, String moduleId) { + return (config.getPlantId().equals(plantId) && config.getModuleId().equals(moduleId)); + } + + /** + * Convenience method to refresh the module programs list from cache. + */ + private void refreshProgramsList() { + final ExpiringCache> localProgramCache = this.programCache; + if (localProgramCache != null) { + final List programs = localProgramCache.getValue(); + if (programs != null) { + dynamicStateDescriptionProvider.setPrograms(programChannelUID, programs); + } + } + } + + /** + * Convenience method to update the given Channel state "only" if the Channel is linked. + * + * @param channelId + * the identifier of the Channel to be updated + * @param state + * the new state to be applied to the given Channel + */ + private void updateChannelState(String channelId, State state) { + final Channel channel = thing.getChannel(channelId); + + if (channel != null && isLinked(channel.getUID())) { + updateState(channel.getUID(), state); + } + } + + /** + * Convenience method to update the whole status of the Chronothermostat associated to this handler. + * Channels are updated based on the local {@code chronothermostat} and {@code moduleSettings} objects. + * + * @throws {@link SmartherIllegalPropertyValueException} + * if at least one of the module properties cannot be mapped to any valid enum value + */ + private void updateModuleStatus() throws SmartherIllegalPropertyValueException { + final Chronothermostat localChrono = this.chronothermostat; + if (localChrono != null) { + // Update the Measures channels + updateChannelState(CHANNEL_MEASURES_TEMPERATURE, localChrono.getThermometer().toState()); + updateChannelState(CHANNEL_MEASURES_HUMIDITY, localChrono.getHygrometer().toState()); + // Update the Status channels + updateChannelState(CHANNEL_STATUS_STATE, (localChrono.isActive() ? OnOffType.ON : OnOffType.OFF)); + updateChannelState(CHANNEL_STATUS_FUNCTION, + new StringType(StringUtil.capitalize(localChrono.getFunction().toLowerCase()))); + updateChannelState(CHANNEL_STATUS_MODE, + new StringType(StringUtil.capitalize(localChrono.getMode().toLowerCase()))); + updateChannelState(CHANNEL_STATUS_TEMPERATURE, localChrono.getSetPointTemperature().toState()); + updateChannelState(CHANNEL_STATUS_ENDTIME, new StringType(localChrono.getActivationTimeLabel())); + updateChannelState(CHANNEL_STATUS_TEMP_FORMAT, new StringType(localChrono.getTemperatureFormat())); + final Program localProgram = localChrono.getProgram(); + if (localProgram != null) { + updateChannelState(CHANNEL_STATUS_PROGRAM, new StringType(String.valueOf(localProgram.getNumber()))); + } + } + + final ModuleSettings localSettings = this.moduleSettings; + if (localSettings != null) { + // Update the Settings channels + updateChannelState(CHANNEL_SETTINGS_MODE, new StringType(localSettings.getMode().getValue())); + updateChannelState(CHANNEL_SETTINGS_TEMPERATURE, localSettings.getSetPointTemperature()); + updateChannelState(CHANNEL_SETTINGS_PROGRAM, new DecimalType(localSettings.getProgram())); + updateChannelState(CHANNEL_SETTINGS_BOOSTTIME, new DecimalType(localSettings.getBoostTime().getValue())); + updateChannelState(CHANNEL_SETTINGS_ENDDATE, new StringType(localSettings.getEndDate())); + updateChannelState(CHANNEL_SETTINGS_ENDHOUR, new DecimalType(localSettings.getEndHour())); + updateChannelState(CHANNEL_SETTINGS_ENDMINUTE, new DecimalType(localSettings.getEndMinute())); + updateChannelState(CHANNEL_SETTINGS_POWER, OnOffType.OFF); + } + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/model/BridgeStatus.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/model/BridgeStatus.java new file mode 100644 index 0000000000000..e98fa1358fd9a --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/model/BridgeStatus.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@code BridgeStatus} class defines the internal status of a Smarther Bridge. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class BridgeStatus { + + private long apiCallsHandled; + private long notificationsReceived; + private long notificationsRejected; + + /** + * Constructs a new {@code BridgeStatus}. + */ + public BridgeStatus() { + this.apiCallsHandled = 0; + this.notificationsReceived = 0; + this.notificationsRejected = 0; + } + + /** + * Returns the total number of API gateway calls made by the bridge. + * + * @return the total number of API calls made. + */ + public long getApiCallsHandled() { + return apiCallsHandled; + } + + /** + * Increment the total number of API gateway calls made by the bridge. + * + * @return the total number of API calls made, after the increment. + */ + public long incrementApiCallsHandled() { + return ++apiCallsHandled; + } + + /** + * Sets the total number of API gateway calls made by the bridge. + * + * @param totalNumber + * the total number of API calls to be set as made + */ + public void setApiCallsHandled(long totalNumber) { + this.apiCallsHandled = totalNumber; + } + + /** + * Returns the total number of module status notifications received by the bridge. + * + * @return the total number of received notifications. + */ + public long getNotificationsReceived() { + return notificationsReceived; + } + + /** + * Increment the total number of module status notifications received by the bridge. + * + * @return the total number of received notification, after the increment. + */ + public long incrementNotificationsReceived() { + return ++notificationsReceived; + } + + /** + * Sets the total number of module status notifications received by the bridge. + * + * @param totalNumber + * the total number of notifications to be set as received + */ + public void setNotificationsReceived(long totalNumber) { + this.notificationsReceived = totalNumber; + } + + /** + * Returns the total number of module status notifications rejected by the bridge. + * + * @return the total number of rejected notifications. + */ + public long getNotificationsRejected() { + return notificationsRejected; + } + + /** + * Increment the total number of module status notifications rejected by the bridge. + * + * @return the total number of rejected notification, after the increment. + */ + public long incrementNotificationsRejected() { + return ++notificationsRejected; + } + + /** + * Sets the total number of module status notifications rejected by the bridge. + * + * @param totalNumber + * the total number of notifications to be set as rejected + */ + public void setNotificationsRejected(long totalNumber) { + this.notificationsRejected = totalNumber; + } + + @Override + public String toString() { + return String.format("apiCallsHandled=%s, notifsReceived=%s, notifsRejected=%s", apiCallsHandled, + notificationsReceived, notificationsRejected); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/model/ModuleSettings.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/model/ModuleSettings.java new file mode 100644 index 0000000000000..10ece5249ee09 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/model/ModuleSettings.java @@ -0,0 +1,315 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.model; + +import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +import javax.measure.Unit; +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.openhab.binding.bticinosmarther.internal.api.dto.Chronothermostat; +import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.BoostTime; +import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.Function; +import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.Mode; +import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherIllegalPropertyValueException; +import org.openhab.binding.bticinosmarther.internal.util.DateUtil; +import org.openhab.binding.bticinosmarther.internal.util.StringUtil; + +/** + * The {@code ModuleSettings} class defines the operational settings of a Smarther Chronothermostat. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public class ModuleSettings { + + private transient String plantId; + private transient String moduleId; + private Function function; + private Mode mode; + private QuantityType setPointTemperature; + private int program; + private BoostTime boostTime; + private @Nullable String endDate; + private int endHour; + private int endMinute; + + /** + * Constructs a {@code ModuleSettings} with the specified plant and module identifiers. + * + * @param plantId + * the identifier of the plant + * @param moduleId + * the identifier of the chronothermostat module inside the plant + */ + public ModuleSettings(String plantId, String moduleId) { + this.plantId = plantId; + this.moduleId = moduleId; + this.function = Function.HEATING; + this.mode = Mode.AUTOMATIC; + this.setPointTemperature = QuantityType.valueOf(7.0, SIUnits.CELSIUS); + this.program = 0; + this.boostTime = BoostTime.MINUTES_30; + this.endDate = null; + this.endHour = 0; + this.endMinute = 0; + } + + /** + * Updates this module settings from a {@link Chronothermostat} dto object. + * + * @param chronothermostat + * the chronothermostat dto to get data from + * + * @throws {@link SmartherIllegalPropertyValueException} + * if at least one of the module properties cannot be mapped to any valid enum value + */ + public void updateFromChronothermostat(Chronothermostat chronothermostat) + throws SmartherIllegalPropertyValueException { + this.function = Function.fromValue(chronothermostat.getFunction()); + } + + /** + * Returns the plant identifier. + * + * @return a string containing the plant identifier. + */ + public String getPlantId() { + return plantId; + } + + /** + * Returns the module identifier. + * + * @return a string containing the module identifier. + */ + public String getModuleId() { + return moduleId; + } + + /** + * Returns the module operational function. + * + * @return a {@link Function} enum representing the module operational function + */ + public Function getFunction() { + return function; + } + + /** + * Returns the module operational mode. + * + * @return a {@link Mode} enum representing the module operational mode + */ + public Mode getMode() { + return mode; + } + + /** + * Sets the module operational mode. + * + * @param mode + * a {@link Mode} enum representing the module operational mode to set + */ + public void setMode(Mode mode) { + this.mode = mode; + } + + /** + * Returns the module operational setpoint temperature for "manual" mode. + * + * @return a {@link QuantityType} object representing the module operational setpoint temperature + */ + public QuantityType getSetPointTemperature() { + return setPointTemperature; + } + + /** + * Returns the module operational setpoint temperature for "manual" mode, using a target unit. + * + * @param targetUnit + * the {@link Unit} unit to convert the setpoint temperature to + * + * @return a {@link QuantityType} object representing the module operational setpoint temperature + */ + public @Nullable QuantityType getSetPointTemperature(Unit targetUnit) { + return setPointTemperature.toUnit(targetUnit); + } + + /** + * Sets the module operational setpoint temperature for "manual" mode. + * + * @param setPointTemperature + * a {@link QuantityType} object representing the setpoint temperature to set + */ + public void setSetPointTemperature(QuantityType setPointTemperature) { + this.setPointTemperature = setPointTemperature; + } + + /** + * Returns the module operational program for "automatic" mode. + * + * @return the module operational program for automatic mode + */ + public int getProgram() { + return program; + } + + /** + * Sets the module operational program for "automatic" mode. + * + * @param program + * the module operational program to set + */ + public void setProgram(int program) { + this.program = program; + } + + /** + * Returns the module operational boost time for "boost" mode. + * + * @return a {@link BoostTime} enum representing the module operational boost time + */ + public BoostTime getBoostTime() { + return boostTime; + } + + /** + * Sets the module operational boost time for "boost" mode. + * + * @param boostTime + * a {@link BoostTime} enum representing the module operational boost time to set + */ + public void setBoostTime(BoostTime boostTime) { + this.boostTime = boostTime; + } + + /** + * Returns the module operational end date for "manual" mode. + * + * @return a string containing the module operational end date, may be {@code null} + */ + public @Nullable String getEndDate() { + return endDate; + } + + /** + * Tells whether the module operational end date for "manual" mode has expired. + * + * @return {@code true} if the end date has expired, {@code false} otherwise + */ + public boolean isEndDateExpired() { + if (endDate != null) { + final LocalDateTime dtEndDate = DateUtil.parseDate(endDate, DTF_DATE).atStartOfDay(); + final LocalDateTime dtToday = LocalDate.now().atStartOfDay(); + + return (dtEndDate.isBefore(dtToday)); + } else { + return false; + } + } + + /** + * Refreshes the module operational end date for "manual" mode, setting it to current local date. + */ + public void refreshEndDate() { + if (endDate != null) { + this.endDate = DateUtil.format(LocalDateTime.now(), DTF_DATE); + } + } + + /** + * Sets the module operational end date for "manual" mode. + * + * @param endDate + * the module operational end date to set + */ + public void setEndDate(String endDate) { + this.endDate = StringUtil.stripToNull(endDate); + } + + /** + * Returns the module operational end hour for "manual" mode. + * + * @return the module operational end hour + */ + public int getEndHour() { + return endHour; + } + + /** + * Sets the module operational end hour for "manual" mode. + * + * @param endHour + * the module operational end hour to set + */ + public void setEndHour(int endHour) { + this.endHour = endHour; + } + + /** + * Returns the module operational end minute for "manual" mode. + * + * @return the module operational end minute + */ + public int getEndMinute() { + return endMinute; + } + + /** + * Sets the module operational end minute for "manual" mode. + * + * @param endMinute + * the module operational end minute to set + */ + public void setEndMinute(int endMinute) { + this.endMinute = endMinute; + } + + /** + * Returns the date and time (format YYYY-MM-DDThh:mm:ss) to which this module settings will be maintained. + * For boost mode a range is returned, as duration is limited to 30, 60 or 90 minutes, indicating starting (current) + * and final date and time. + * + * @return a string containing the module settings activation time, or and empty ("") string if the module operation + * mode doesn't allow for an activation time + */ + public String getActivationTime() { + if (mode.equals(Mode.MANUAL) && (endDate != null)) { + LocalDateTime d = DateUtil.parseDate(endDate, DTF_DATE).atTime(endHour, endMinute); + return DateUtil.format(d, DTF_DATETIME); + } else if (mode.equals(Mode.BOOST)) { + LocalDateTime d1 = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES); + LocalDateTime d2 = d1.plusMinutes(boostTime.getValue()); + return DateUtil.formatRange(d1, d2, DTF_DATETIME); + } else { + return ""; + } + } + + @Override + public String toString() { + return String.format( + "plantId=%s, moduleId=%s, mode=%s, setPointTemperature=%s, program=%s, boostTime=%s, endDate=%s, endHour=%s, endMinute=%s", + plantId, moduleId, mode, setPointTemperature, program, boostTime, endDate, endHour, endMinute); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/util/DateUtil.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/util/DateUtil.java new file mode 100644 index 0000000000000..db94a6656ff44 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/util/DateUtil.java @@ -0,0 +1,181 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.util; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@code DateUtil} class defines common date utility functions used across the whole binding. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public final class DateUtil { + + private static final String RANGE_FORMAT = "%s/%s"; + + /** + * Parses a local date contained in the given string, using the given pattern. + * + * @param str + * the string to be parsed (can be {@code null}) + * @param pattern + * the pattern to be used to parse the given string + * + * @return a {@link LocalDate} object containing the parsed date + * + * @throws {@link DateTimeParseException} + * if the string cannot be parsed to a local date + */ + public static LocalDate parseDate(@Nullable String str, String pattern) { + DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern); + return LocalDate.parse(str, dtf); + } + + /** + * Parses a local date and time contained in the given string, using the given pattern. + * + * @param str + * the string to be parsed (can be {@code null}) + * @param pattern + * the pattern to be used to parse the given string + * + * @return a {@link LocalDateTime} object containing the parsed date and time + * + * @throws {@link DateTimeParseException} + * if the string cannot be parsed to a local date and time + */ + public static LocalDateTime parseLocalTime(@Nullable String str, String pattern) { + DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern); + return LocalDateTime.parse(str, dtf); + } + + /** + * Parses a date and time with timezone contained in the given string, using the given pattern. + * + * @param str + * the string to be parsed (can be {@code null}) + * @param pattern + * the pattern to be used to parse the given string + * + * @return a {@link ZonedDateTime} object containing the parsed date and time with timezone + * + * @throws {@link DateTimeParseException} + * if the string cannot be parsed to a date and time with timezone + */ + public static ZonedDateTime parseZonedTime(@Nullable String str, String pattern) { + DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern); + return ZonedDateTime.parse(str, dtf); + } + + /** + * Returns a date at given days after today and at start of day in the given timezone. + * + * @param days + * the number of days to be added ({@code 0} means today) + * @param zoneId + * the identifier of the timezone to be applied + * + * @return a {@link ZonedDateTime} object containing the date and time with timezone + */ + public static ZonedDateTime getZonedStartOfDay(int days, ZoneId zoneId) { + return LocalDate.now().plusDays(days).atStartOfDay(zoneId); + } + + /** + * Returns a string representing the given local date and time object, using the given format pattern. + * + * @param date + * the local date and time object to be formatted + * @param pattern + * the format pattern to be applied + * + * @return a string representing the local date and time object + * + * @throws {@link DateTimeException} + * if an error occurs during printing + */ + public static String format(LocalDateTime date, String pattern) { + DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern); + return date.format(dtf); + } + + /** + * Returns a string representing the given date and time with timezone object, using the given format pattern. + * + * @param date + * the date and time with timezone object to be formatted + * @param pattern + * the format pattern to be applied + * + * @return a string representing the date and time with timezone object + * + * @throws {@link DateTimeException} + * if an error occurs during printing + */ + public static String format(ZonedDateTime date, String pattern) { + DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern); + return date.format(dtf); + } + + /** + * Returns a string representing the range between two local date and time objects, using the given format pattern. + * The range itself is returned as {@code /}. + * + * @param date1 + * the first local date and time object in range + * @param date2 + * the second local date and time object in range + * @param pattern + * the format pattern to be applied + * + * @return a string representing the range between the two local date and time objects + * + * @throws {@link DateTimeException} + * if an error occurs during printing + */ + public static String formatRange(LocalDateTime date1, LocalDateTime date2, String pattern) { + DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern); + return String.format(RANGE_FORMAT, date1.format(dtf), date2.format(dtf)); + } + + /** + * Returns a string representing the range between two date and time with timezone objects, using the given format + * pattern. + * The range itself is returned as {@code /}. + * + * @param date1 + * the first date and time with timezone object in range + * @param date2 + * the second date and time with timezone object in range + * @param pattern + * the format pattern to be applied + * + * @return a string representing the range between the two date and time with timezone objects + * + * @throws {@link DateTimeException} + * if an error occurs during printing + */ + public static String formatRange(ZonedDateTime date1, ZonedDateTime date2, String pattern) { + DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern); + return String.format(RANGE_FORMAT, date1.format(dtf), date2.format(dtf)); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/util/ModelUtil.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/util/ModelUtil.java new file mode 100644 index 0000000000000..221bbe5a481f0 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/util/ModelUtil.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.util; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * The {@code ModelUtil} utility class to get the {@code Gson} instance to parse the Smarther API data with. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public final class ModelUtil { + + private static final Gson GSON = new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + + private ModelUtil() { + // Util class + } + + /** + * Returns the {@code Gson} instance to parse the Smarther API data with. + * + * @return the {@code Gson} instance + */ + public static Gson gsonInstance() { + return GSON; + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/util/StringUtil.java b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/util/StringUtil.java new file mode 100644 index 0000000000000..2ca5e8308b13d --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/java/org/openhab/binding/bticinosmarther/internal/util/StringUtil.java @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bticinosmarther.internal.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@code StringUtil} class defines common string utility functions used across the whole binding. + * + * @author Fabio Possieri - Initial contribution + */ +@NonNullByDefault +public final class StringUtil { + + private static final int EOF = -1; + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + /** + * Checks if a string is whitespace, empty ("") or {@code null}. + * + * @param str + * the string to check, may be {@code null} + * + * @return {@code true} if the string is {@code null}, empty or whitespace + */ + public static boolean isBlank(@Nullable String str) { + return (str == null || str.trim().isEmpty()); + } + + /** + * Returns either the passed in string or, if the string is {@code null}, an empty string (""). + * + * @param str + * the string to check, may be {@code null} + * + * @return the passed in string, or the empty string if it was {@code null} + * + */ + public static final String defaultString(@Nullable String str) { + return (str == null) ? "" : str; + } + + /** + * Returns either the passed in string or, if the string is whitespace, empty ("") or {@code null}, a default value. + * + * @param str + * the string to check, may be {@code null} + * @param defaultStr + * the default string to return + * + * @return the passed in string, or the default one + */ + public static String defaultIfBlank(String str, String defaultStr) { + return StringUtil.isBlank(str) ? defaultStr : str; + } + + /** + * Strips whitespace from the start and end of a string returning {@code null} if the string is empty ("") after the + * strip. + * + * @param str + * the string to be stripped, may be {@code null} + * + * @return the stripped string, {@code null} if whitespace, empty or {@code null} input string + */ + public static @Nullable String stripToNull(@Nullable String str) { + if (str == null) { + return null; + } + String s = str.trim(); + return (s.isEmpty()) ? null : s; + } + + /** + * Capitalizes a string changing the first letter to title case as per {@link Character#toTitleCase(char)}. No other + * letters are changed. + * + * @param str + * the string to capitalize, may be {@code null} + * + * @return the capitalized string, {@code null} if {@code null} input string + */ + public static @Nullable String capitalize(@Nullable String str) { + if (str == null || str.isEmpty()) { + return str; + } + return str.substring(0, 1).toUpperCase() + str.substring(1); + } + + /** + * Converts all the whitespace separated words in a string into capitalized words, that is each word is made up of a + * titlecase character and then a series of lowercase characters. + * + * @param str + * the string to capitalize, may be {@code null} + * + * @return the capitalized string, {@code null} if {@code null} input string + */ + public static @Nullable String capitalizeAll(@Nullable String str) { + if (str == null || str.isEmpty()) { + return str; + } + // Java 8 version + return Arrays.stream(str.split("\\s+")).map(t -> t.substring(0, 1).toUpperCase() + t.substring(1).toLowerCase()) + .collect(Collectors.joining(" ")); + // Ready for Java 9+ + // return Pattern.compile("\\b(.)(.*?)\\b").matcher(str) + // .replaceAll(match -> match.group(1).toUpperCase() + match.group(2).toLowerCase()); + } + + /** + * Get the contents of an {@link InputStream} stream as a string using the default character encoding of the + * platform. This method buffers the input internally, so there is no need to use a {@code BufferedInputStream}. + * + * @param input + * the {@code InputStream} to read from + * + * @return the string read from stream + * + * @throws {@link IOException} + * if an I/O error occurs + */ + public static String streamToString(InputStream input) throws IOException { + InputStreamReader reader = new InputStreamReader(input); + + final StringWriter writer = new StringWriter(); + char[] buffer = new char[DEFAULT_BUFFER_SIZE]; + + int n = 0; + while ((n = reader.read(buffer)) != EOF) { + writer.write(buffer, 0, n); + } + + return writer.toString(); + } + + /** + * Get the contents of a {@link Reader} stream as a string using the default character encoding of the platform. + * This method doesn't buffer the input internally, so eventually {@code BufferedReder} needs to be used externally. + * + * @param reader + * the {@code Reader} to read from + * + * @return the string read from stream + * + * @throws {@link IOException} + * if an I/O error occurs + */ + public static String readerToString(Reader reader) throws IOException { + final StringWriter writer = new StringWriter(); + + int c; + while ((c = reader.read()) != EOF) { + writer.write(c); + } + + return writer.toString(); + } + +} diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..ced6a7fa69cd9 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + BTicino Smarther Binding + This is the binding for BTicino Smarther chronothermostat units + Fabio Possieri + + diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/config/config.xml b/bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/config/config.xml new file mode 100644 index 0000000000000..6072e93e6dbe2 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/config/config.xml @@ -0,0 +1,141 @@ + + + + + + + + + + Details of the Smarther product subscription connected to the BTicino/Legrand development account. + + + + + Details of the Smarther application registered on the BTicino/Legrand development portal. + + + + + Advanced settings of this bridge. + + + + + + This is the Subscription Key provided by BTicino/Legrand when you subscribe to Smarther - v2.0 product. Go to +https://developer.legrand.com/tutorials/getting-started/ + true + + + + + This is the Client ID provided by BTicino/Legrand when you add a new Application to your developer account. Go to +https://developer.legrand.com/tutorials/create-an-application/ + true + + + + + This is the Client Secret provided by BTicino/Legrand when you add a new Application to your developer account. + true + password + + + + + ON = the bridge subscribes each of its locations to receive C2C notifications upon changes on each of its modules' status or sensors +data - temperature, humidity (requires a public https endpoint has been set as "First Reply Url" when registering the Application on Legrand's development +portal); OFF = for each module connected to this bridge, status+sensors data are requested to Smarther API gateway on a periodical basis and whenever new +settings are applied (period can be changed via module's "Status Refresh Period" parameter). + false + true + true + + + + + This is the frequency the Smarther API gateway is called to update bridge status. There are limits to the number of +requests that can be sent to the Smarther API gateway. The more often you poll, the faster locations are updated - at the risk of running out of your request +quota. + false + true + Minutes + 1440 + + + + + + + + + + + Reference to uniquely identify the module towards the BTicino/Legrand API gateway. + + + + + Advanced settings of this module. + + + + + + This is the Plant Id of the location the Chronothermostat module is installed in, provided by Smarther API. + true + + + + + This is the Module Id of the Chronothermostat module, provided by Smarther API. + true + + + + + ON = the module settings are automatically updated according to the module status whenever it changes (e.g. polling, notification, +etc.). OFF = the module settings are aligned to the module status only upon module initialization. + false + true + false + + + + + This is the frequency the Smarther API gateway is called to refresh Programs list used in "automatic" mode. There are limits to the +number of requests that can be sent to the Smarther API gateway. The more often you poll, the faster locations are updated - at the risk of running out of your +request quota. + false + true + Hours + 12 + + + + + This is the number of days to be displayed in module settings, as options list for "End Date" field in "manual" mode (e.g. 1 = only +"Today" is displayed, 5 = "Today" + "Tomorrow" + following 3 days are displayed). + false + true + 5 + + + + + This is the frequency the Smarther API gateway is called to update module status and sensors data. There are limits to the number of +requests that can be sent to the Smarther API gateway. The more often you poll, the faster locations are updated - at the risk of running out of your request +quota. + false + true + Minutes + 60 + + + + + diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/thing/bridge-types.xml b/bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/thing/bridge-types.xml new file mode 100644 index 0000000000000..3536e931e90e4 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/thing/bridge-types.xml @@ -0,0 +1,101 @@ + + + + + + + + + If you want to control your devices in the context of different accounts you have to register a bridge for each account.
+
+ How-To configure the bridge:
+
    +
  • Sign up for a new developer account on Works with Legrand website
  • +
  • Subscribe to "Starter Kit for Legrand APIs" from API > Subscriptions menu +
      +
    • This will generate your primary and secondary "Subscription Key"
    • +
    +
  • +
  • Register a new application from User > My Applications menu +
      +
    • In "First Reply Url" field insert the public callback URL "https://<your openHAB host>:<your openHAB port>/smarther/connectsmarther"
    • +
    • Tick the checkbox near "comfort.read" and "comfort.write" scopes
    • +
    + You should receive an email from Legrand, usually within 1-2 days max, containing your application's "Client ID" and "Client Secret". +
  • +
+ How-To authorize the bridge:
+
    +
  • Create and configure a bridge Thing first, using above Subscription Key + Client ID + Client Secret, then
  • +
  • Open in your browser the public URL "https://<your openHAB host>:<your openHAB port>/smarther/connectsmarther", and
  • +
  • Follow the steps reported therein to authorize the bridge
  • +
+ ]]> +
+ + + + + + + + BTicino + + + subscriptionKey + + +
+ + + + + Current operational status of the bridge + + + + + + + + + + Convenience configuration channels for the bridge + + + + + + + + Number + + Total number of API calls handled by the bridge + + + + + Number + + Total number of C2C notifications received by the bridge + + + + + Number + + Total number of C2C notifications rejected by the bridge + + + + + Switch + + This is a convenience switch to trigger a call to the Smarther API gateway, to manually fetch the updated client locations list. + + +
diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..df06f8db3cad9 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/resources/ESH-INF/thing/thing-types.xml @@ -0,0 +1,221 @@ + + + + + + + + + + + This thing represents a BTicino Smarther chronothermostat module. + + + + + + + + + + BTicino + X8000 + + + moduleId + + + + + + + + Measures taken from the module on-board sensors + + + + + + + + + Current operational status of the module + + + + + + + + + + + + + + New operational settings to be applied to the module + + + + + + + + + + + + + + + Convenience configuration channels for the module + + + + + + + + Number:Temperature + + Indoor temperature as measured by the sensor + Temperature + + + + + Number:Dimensionless + + Indoor humidity as measured by the sensor + Humidity + + + + + Switch + + Current operational state of the module + + + + + String + + Current operational function set on the module + + + + + String + + Current operational mode set on the module + + + + + Number:Temperature + + Current operational target temperature set on the module + Temperature + + + + + String + + Current operational program set on the module + + + + + String + + Current operational end time set on the module + + + + + String + + Current operational temperature format of the module + + + + + String + + New operational mode to be set on the module + + + + + + + + + + + + + Number:Temperature + + New operational set-point temperature to be set on the module (valid only for Mode = "Manual") + Temperature + + + + + Number + + New operational program to be set on the module (valid only for Mode = "Automatic") + + + + Number + + New operational boost time to be set on the module (valid only for Mode = "Boost") + + + + + + + + + + + String + + New operational end date to be set on the module (valid only for Mode = "Manual") + + + + Number + + New operational end hour to be set on the module (valid only for Mode = "Manual") + + + + + Number + + New operational end minute to be set on the module (valid only for Mode = "Manual") + + + + + Switch + + Power on, send new operational settings to the module + + + + Switch + + This is a convenience switch to trigger a call to the Smarther API gateway, to manually fetch the updated module programs list. + + + diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/resources/templates/application.html b/bundles/org.openhab.binding.bticinosmarther/src/main/resources/templates/application.html new file mode 100644 index 0000000000000..5a8140efb38e3 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/resources/templates/application.html @@ -0,0 +1,4 @@ +
+ Connect to Smarther API gateway: ${application.name} ${application.locations} + +
diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/resources/templates/index.html b/bundles/org.openhab.binding.bticinosmarther/src/main/resources/templates/index.html new file mode 100644 index 0000000000000..14099cc816763 --- /dev/null +++ b/bundles/org.openhab.binding.bticinosmarther/src/main/resources/templates/index.html @@ -0,0 +1,95 @@ + + + + + +${pageRefresh} +Authorize openHAB binding for BTicino Smarther Chronothermostats + + + + +
+ + +

Authorize openHAB binding for BTicino Smarther Chronothermostats

+

On this page you can authorize your openHAB Smarther Bridge configured with the Subscription Key, Client Id and Client Secret of the Smarther Application on your developer account.

+

You have to login to your BTicino/Legrand developer account, in order to authorize this binding to access your account and connected devices.

+

To use this binding the following requirements apply:

+
    +
  • A BTicino/Legrand developer account. +
  • Subscribe to Smarther - v2.0 product, to obtain the Subscription Key +
  • Register an Application on your developer account. +
+

+ The redirect URI to use when registering an Applicaton for this openHAB installation is + ${redirectUri} +

+ ${error} ${authorizedBridge} ${applications} +
+ + diff --git a/bundles/org.openhab.binding.bticinosmarther/src/main/resources/web/favicon.ico b/bundles/org.openhab.binding.bticinosmarther/src/main/resources/web/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..070fbb11c94c823be9e67bfd8b8d3e2bcb3820c4 GIT binary patch literal 32038 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 w + + 4.0.0 @@ -60,6 +62,7 @@ org.openhab.binding.boschindego org.openhab.binding.bosesoundtouch org.openhab.binding.bsblan + org.openhab.binding.bticinosmarther org.openhab.binding.buienradar org.openhab.binding.cbus org.openhab.binding.chromecast From 4c2f4f62fde62ec99ada51fd414c7ce3564cc7d2 Mon Sep 17 00:00:00 2001 From: mmans Date: Mon, 29 Jun 2020 20:27:26 +0200 Subject: [PATCH 21/85] [innogysmarthome] Updated Auth-links in docs (#8039) Signed-off-by: Marco Mans --- bundles/org.openhab.binding.innogysmarthome/README.md | 8 ++++---- .../ESH-INF/i18n/innogysmarthome_de_DE.properties | 2 +- .../src/main/resources/ESH-INF/thing/bridge.xml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bundles/org.openhab.binding.innogysmarthome/README.md b/bundles/org.openhab.binding.innogysmarthome/README.md index c9f5361562d3b..99abded64376a 100644 --- a/bundles/org.openhab.binding.innogysmarthome/README.md +++ b/bundles/org.openhab.binding.innogysmarthome/README.md @@ -117,10 +117,10 @@ Be sure it is connected to the Internet. Authorization is done as oauth2 workflow with the innogy API. To receive the auth-code, go to one of the following URLs depending on your brand and login with your credentials (you can find this link also in the SHC thing in Paper UI, if you edit it): - -* [innogy SmartHome authorization page](https://api.services-smarthome.de/AUTH/authorize?response_type=code&client_id=24635748&redirect_uri=https%3A%2F%2Fwww.openhab.org%2Foauth%2Finnogy%2Finnogy-smarthome.html&scope&lang=de-DE) -* [SmartHome Austria authorization page](https://api.services-smarthome.de/AUTH/authorize?response_type=code&client_id=24635749&redirect_uri=https%3A%2F%2Fwww.openhab.org%2Foauth%2Finnogy%2Fsmarthome-austria.html&scope&lang=de-DE) -* [Start SmartHome authorization page](https://api.services-smarthome.de/AUTH/authorize?response_type=code&client_id=24635750&redirect_uri=https%3A%2F%2Fwww.openhab.org%2Foauth%2Finnogy%2Fstart-smarthome.html&scope&lang=de-DE) +https://auth.services-smarthome.de/AUTH +* [innogy SmartHome authorization page](https://auth.services-smarthome.de/AUTH/authorize?response_type=code&client_id=24635748&redirect_uri=https%3A%2F%2Fwww.openhab.org%2Foauth%2Finnogy%2Finnogy-smarthome.html&scope&lang=de-DE) +* [SmartHome Austria authorization page](https://auth.services-smarthome.de/AUTH/authorize?response_type=code&client_id=24635749&redirect_uri=https%3A%2F%2Fwww.openhab.org%2Foauth%2Finnogy%2Fsmarthome-austria.html&scope&lang=de-DE) +* [Start SmartHome authorization page](https://auth.services-smarthome.de/AUTH/authorize?response_type=code&client_id=24635750&redirect_uri=https%3A%2F%2Fwww.openhab.org%2Foauth%2Finnogy%2Fstart-smarthome.html&scope&lang=de-DE) You will be redirected to openhab.org and the auth-code will be displayed. Copy and paste it into your SHC configuration and you are done. diff --git a/bundles/org.openhab.binding.innogysmarthome/src/main/resources/ESH-INF/i18n/innogysmarthome_de_DE.properties b/bundles/org.openhab.binding.innogysmarthome/src/main/resources/ESH-INF/i18n/innogysmarthome_de_DE.properties index d19b4c3c1bac1..dcdb82cfb2234 100644 --- a/bundles/org.openhab.binding.innogysmarthome/src/main/resources/ESH-INF/i18n/innogysmarthome_de_DE.properties +++ b/bundles/org.openhab.binding.innogysmarthome/src/main/resources/ESH-INF/i18n/innogysmarthome_de_DE.properties @@ -10,7 +10,7 @@ thing-type.config.innogysmarthome.bridge.connection.description = Parameter zur thing-type.config.innogysmarthome.bridge.brand.label = Marke thing-type.config.innogysmarthome.bridge.brand.description = Whle die Marke Deiner innogy SmartHome Lsung. thing-type.config.innogysmarthome.bridge.authcode.label = Autorisierungscode -thing-type.config.innogysmarthome.bridge.authcode.description = Der Autorisierungscode ist ein einmaliger Code, um die notwendigen Zugangscodes vom innogy SmartHome Dienst zu erhalten. Bitte gehe je nach Marke zum...
  • innogy SmartHome Portal: https://api.services-smarthome.de/AUTH/authorize?response_type=code&client_id=24635748&redirect_uri=https%3A%2F%2Fwww.openhab.org%2Foauth%2Finnogy%2Finnogy-smarthome.html&scope&lang=de-DE
... um einen Autorisierungscode zu erstellen und kopiere ihn hier hinein.. Nach der einmaligen Autorisierung verfllt der Code und wird nicht mehr bentigt. +thing-type.config.innogysmarthome.bridge.authcode.description = Der Autorisierungscode ist ein einmaliger Code, um die notwendigen Zugangscodes vom innogy SmartHome Dienst zu erhalten. Bitte gehe je nach Marke zum...
  • innogy SmartHome Portal: https://auth.services-smarthome.de/AUTH/authorize?response_type=code&client_id=24635748&redirect_uri=https%3A%2F%2Fwww.openhab.org%2Foauth%2Finnogy%2Finnogy-smarthome.html&scope&lang=de-DE
... um einen Autorisierungscode zu erstellen und kopiere ihn hier hinein.. Nach der einmaligen Autorisierung verfllt der Code und wird nicht mehr bentigt. thing-type.config.innogysmarthome.bridge.websocketidletimeout.label = WebSocket idle timeout in Sekunden thing-type.config.innogysmarthome.bridge.websocketidletimeout.description = Der WebSocket hlt die Verbindung zum innogy Webservice und wartet auf Statusaktualisierungen. Wenn fr die angegebene Dauer keine Daten ber den WebSocket empfangen werden, wird die Verbindung neu aufgebaut. 0 deaktiviert den idle timeout. Standard ist 900 Sekunden (15 Minuten). thing-type.config.innogysmarthome.bridge.refreshtoken.label = Refresh-Token diff --git a/bundles/org.openhab.binding.innogysmarthome/src/main/resources/ESH-INF/thing/bridge.xml b/bundles/org.openhab.binding.innogysmarthome/src/main/resources/ESH-INF/thing/bridge.xml index c3a42a9d76d10..f26133b09f370 100644 --- a/bundles/org.openhab.binding.innogysmarthome/src/main/resources/ESH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.innogysmarthome/src/main/resources/ESH-INF/thing/bridge.xml @@ -38,7 +38,7 @@ Please go - depending on your brand - to...
    -
  • innogy SmartHome Portal: https://api.services-smarthome.de/AUTH/authorize?response_type=code&client_id=24635748&redirect_uri=https%3A%2F%2Fwww.openhab.org%2Foauth%2Finnogy%2Finnogy-smarthome.html&scope&lang=de-DE
  • +
  • innogy SmartHome Portal: https://auth.services-smarthome.de/AUTH/authorize?response_type=code&client_id=24635748&redirect_uri=https%3A%2F%2Fwww.openhab.org%2Foauth%2Finnogy%2Finnogy-smarthome.html&scope&lang=de-DE
... to generate an auth-code and paste it here. After initial authorization, this code is not needed anymore.]]>
From 3653edbf47d2d98673c37f4098bc2202d9d5b737 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Mon, 29 Jun 2020 20:50:53 +0200 Subject: [PATCH 22/85] [samsungtv] cleanup websocket shutdown (#8025) * cleanup shutdown * add null check * fix codestyle Signed-off-by: Jan N. Klug --- bundles/org.openhab.binding.samsungtv/pom.xml | 4 +++- .../protocol/RemoteControllerWebSocket.java | 14 ++++++++------ .../samsungtv/internal/protocol/WebSocketBase.java | 13 ++++++++++++- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/bundles/org.openhab.binding.samsungtv/pom.xml b/bundles/org.openhab.binding.samsungtv/pom.xml index a9e4d6200955d..a9029e1cb017d 100644 --- a/bundles/org.openhab.binding.samsungtv/pom.xml +++ b/bundles/org.openhab.binding.samsungtv/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 diff --git a/bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/protocol/RemoteControllerWebSocket.java b/bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/protocol/RemoteControllerWebSocket.java index f9e9f8be6a9f2..e4b18b229bb50 100644 --- a/bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/protocol/RemoteControllerWebSocket.java +++ b/bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/protocol/RemoteControllerWebSocket.java @@ -109,7 +109,7 @@ public String toString() { * @param port TCP port of the remote controller protocol. * @param appName Application name used to send key codes. * @param uniqueId Unique Id used to send key codes. - * @param remoteControllerService + * @param remoteControllerWebsocketCallback callback * @throws RemoteControllerException */ public RemoteControllerWebSocket(String host, int port, String appName, String uniqueId, @@ -123,7 +123,7 @@ public RemoteControllerWebSocket(String host, int port, String appName, String u throw new RemoteControllerException("No WebSocketFactory available"); } - client = webSocketFactory.createWebSocketClient("samtungtv"); + client = webSocketFactory.createWebSocketClient("samsungtv"); client.addLifeCycleListener(this); @@ -194,11 +194,13 @@ private void connectWebSockets() { private void closeConnection() throws RemoteControllerException { logger.debug("RemoteControllerWebSocket closeConnection"); - webSocketRemote.close(); - webSocketArt.close(); - webSocketV2.close(); try { - client.stop(); + webSocketRemote.close(); + webSocketArt.close(); + webSocketV2.close(); + if (client.isStarted()) { + client.stop(); + } } catch (Exception e) { throw new RemoteControllerException(e); } diff --git a/bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/protocol/WebSocketBase.java b/bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/protocol/WebSocketBase.java index 726fa73a17725..37c0f7a6a5f06 100644 --- a/bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/protocol/WebSocketBase.java +++ b/bundles/org.openhab.binding.samsungtv/src/main/java/org/openhab/binding/samsungtv/internal/protocol/WebSocketBase.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.net.URI; +import java.util.concurrent.Future; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -35,6 +36,8 @@ class WebSocketBase extends WebSocketAdapter { */ final RemoteControllerWebSocket remoteControllerWebSocket; + private @Nullable Future sessionFuture; + /** * @param remoteControllerWebSocket */ @@ -72,7 +75,8 @@ void connect(URI uri) throws RemoteControllerException { isConnecting = true; try { - remoteControllerWebSocket.client.connect(this, uri); + sessionFuture = remoteControllerWebSocket.client.connect(this, uri); + logger.trace("Connecting session Future: {}", sessionFuture); } catch (IOException | IllegalStateException e) { throw new RemoteControllerException(e); } @@ -89,10 +93,17 @@ public void onWebSocketConnect(@Nullable Session session) { void close() { logger.debug("{} connection close requested", this.getClass().getSimpleName()); + Session session = getSession(); if (session != null) { session.close(); } + + final Future sessionFuture = this.sessionFuture; + logger.trace("Closing session Future: {}", sessionFuture); + if (sessionFuture != null && !sessionFuture.isDone()) { + sessionFuture.cancel(true); + } } void sendCommand(String cmd) { From 6233f90cb01f634a91016014f471d5f20e346803 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 29 Jun 2020 17:12:35 -0700 Subject: [PATCH 23/85] [miio] add philips.light.hbulb Wi-Fi Bulb E27 White (#8034) adding Philips Wi-Fi Bulb E27 White Closes #7947 Signed-off-by: Marcel Verpaalen --- bundles/org.openhab.binding.miio/README.md | 28 +++++++++++++++++++ .../binding/miio/internal/MiIoDevices.java | 1 + .../database/philips.light.bulb.json | 3 +- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.miio/README.md b/bundles/org.openhab.binding.miio/README.md index 6f1d6d3f483f4..01952370a0cb1 100644 --- a/bundles/org.openhab.binding.miio/README.md +++ b/bundles/org.openhab.binding.miio/README.md @@ -137,6 +137,7 @@ However, for devices that are unsupported, you may override the value and try to | Xiaomi Philips LED Ceiling Lamp | miio:basic | [philips.light.ceiling](#philips-light-ceiling) | Yes | | | Xiaomi Philips LED Ceiling Lamp | miio:basic | [philips.light.zyceiling](#philips-light-zyceiling) | Yes | | | Xiaomi Philips Bulb | miio:basic | [philips.light.bulb](#philips-light-bulb) | Yes | | +| Xiaomi Philips Wi-Fi Bulb E27 White | miio:basic | [philips.light.hbulb](#philips-light-hbulb) | Yes | | | PHILIPS Zhirui Smart LED Bulb E14 Candle Lamp | miio:basic | [philips.light.candle](#philips-light-candle) | Yes | | | Xiaomi Philips Downlight | miio:basic | [philips.light.downlight](#philips-light-downlight) | Yes | | | Xiaomi Philips ZhiRui bedside lamp | miio:basic | [philips.light.moonlight](#philips-light-moonlight) | Yes | | @@ -1023,6 +1024,18 @@ e.g. `smarthome:send actionCommand 'upd_timer["1498595904821", "on"]'` would ena | switchscene | Switch | Switch Scene | | delayoff | Switch | Delay Off | +### Xiaomi Philips Wi-Fi Bulb E27 White (philips.light.hbulb) Channels + +| Channel | Type | Description | +|------------------|---------|-------------------------------------| +| power | Switch | Power | +| brightness | Dimmer | Brightness | +| cct | Dimmer | Correlated Color Temperature | +| scene | Number | Scene | +| dv | Number | DV | +| switchscene | Switch | Switch Scene | +| delayoff | Switch | Delay Off | + ### PHILIPS Zhirui Smart LED Bulb E14 Candle Lamp (philips.light.candle) Channels | Channel | Type | Description | @@ -2592,6 +2605,21 @@ Switch switchscene "Switch Scene" (G_light) {channel="miio:basic:light:switchsce Switch delayoff "Delay Off" (G_light) {channel="miio:basic:light:delayoff"} ``` +### Xiaomi Philips Wi-Fi Bulb E27 White (philips.light.hbulb) item file lines + +note: Autogenerated example. Replace the id (light) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +```java +Group G_light "Xiaomi Philips Wi-Fi Bulb E27 White" +Switch power "Power" (G_light) {channel="miio:basic:light:power"} +Dimmer brightness "Brightness" (G_light) {channel="miio:basic:light:brightness"} +Dimmer cct "Correlated Color Temperature" (G_light) {channel="miio:basic:light:cct"} +Number scene "Scene" (G_light) {channel="miio:basic:light:scene"} +Number dv "DV" (G_light) {channel="miio:basic:light:dv"} +Switch switchscene "Switch Scene" (G_light) {channel="miio:basic:light:switchscene"} +Switch delayoff "Delay Off" (G_light) {channel="miio:basic:light:delayoff"} +``` + ### PHILIPS Zhirui Smart LED Bulb E14 Candle Lamp (philips.light.candle) item file lines note: Autogenerated example. Replace the id (light) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java index 244e0735b480d..02e89fd62e51a 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java @@ -86,6 +86,7 @@ public enum MiIoDevices { PHILIPS_C("philips.light.ceiling", "Xiaomi Philips LED Ceiling Lamp", THING_TYPE_BASIC), PHILIPS_C2("philips.light.zyceiling", "Xiaomi Philips LED Ceiling Lamp", THING_TYPE_BASIC), PHILIPS_BULB("philips.light.bulb", "Xiaomi Philips Bulb", THING_TYPE_BASIC), + PHILIPS_HBULB("philips.light.hbulb", "Xiaomi Philips Wi-Fi Bulb E27 White", THING_TYPE_BASIC), PHILIPS_CANDLE("philips.light.candle", "PHILIPS Zhirui Smart LED Bulb E14 Candle Lamp", THING_TYPE_BASIC), PHILIPS_DOWN("philips.light.downlight", "Xiaomi Philips Downlight", THING_TYPE_BASIC), PHILIPS_MOON("philips.light.moonlight", "Xiaomi Philips ZhiRui bedside lamp", THING_TYPE_BASIC), diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/philips.light.bulb.json b/bundles/org.openhab.binding.miio/src/main/resources/database/philips.light.bulb.json index 4daa33fa26373..4efe1b8479212 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/database/philips.light.bulb.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/philips.light.bulb.json @@ -5,7 +5,8 @@ "philips.light.downlight", "philips.light.virtual", "philips.light.zysread", - "philips.light.zystrip" + "philips.light.zystrip", + "philips.light.hbulb" ], "channels": [ { From 447947228ba7594d25d7aaab6f23b1bd9f21192d Mon Sep 17 00:00:00 2001 From: Benjamin Lafois Date: Tue, 30 Jun 2020 20:39:54 +0200 Subject: [PATCH 24/85] [bluetooth.daikinmadoka] Daikinmadoka initial contribution (#7434) Signed-off-by: Benjamin Lafois --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../.classpath | 32 + .../.project | 23 + .../NOTICE | 13 + .../README.md | 110 +++ .../pom.xml | 26 + .../src/main/feature/feature.xml | 10 + .../DaikinMadokaBindingConstants.java | 55 ++ .../handler/DaikinMadokaHandler.java | 772 ++++++++++++++++++ .../internal/BRC1HUartProcessor.java | 103 +++ .../internal/DaikinMadokaConfiguration.java | 26 + .../internal/DaikinMadokaHandlerFactory.java | 66 ++ .../internal/model/MadokaMessage.java | 154 ++++ .../model/MadokaParsingException.java | 35 + .../internal/model/MadokaProperties.java | 81 ++ .../internal/model/MadokaSettings.java | 128 +++ .../internal/model/MadokaValue.java | 82 ++ .../internal/model/commands/BRC1HCommand.java | 114 +++ .../model/commands/GetFanspeedCommand.java | 80 ++ .../commands/GetIndoorOutoorTemperatures.java | 94 +++ .../commands/GetOperationmodeCommand.java | 69 ++ .../model/commands/GetPowerstateCommand.java | 69 ++ .../model/commands/GetSetpointCommand.java | 78 ++ .../model/commands/GetVersionCommand.java | 79 ++ .../model/commands/ResponseListener.java | 48 ++ .../model/commands/SetFanspeedCommand.java | 76 ++ .../commands/SetOperationmodeCommand.java | 68 ++ .../model/commands/SetPowerstateCommand.java | 70 ++ .../model/commands/SetSetpointCommand.java | 83 ++ .../resources/ESH-INF/thing/daikinmadoka.xml | 133 +++ .../daikinmadoka/handler/DummyThing.java | 146 ++++ .../internal/MadokaMessageTest.java | 46 ++ bundles/pom.xml | 1 + .../src/main/resources/footer.xml | 1 + 35 files changed, 2977 insertions(+) create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/.classpath create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/.project create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/NOTICE create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/README.md create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/pom.xml create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/DaikinMadokaBindingConstants.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/BRC1HUartProcessor.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/DaikinMadokaConfiguration.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/DaikinMadokaHandlerFactory.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaMessage.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaParsingException.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaProperties.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaSettings.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaValue.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/BRC1HCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetFanspeedCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetIndoorOutoorTemperatures.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetOperationmodeCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetPowerstateCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetSetpointCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetVersionCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/ResponseListener.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetFanspeedCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetOperationmodeCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetPowerstateCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetSetpointCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/resources/ESH-INF/thing/daikinmadoka.xml create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/test/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DummyThing.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/test/java/org/openhab/binding/bluetooth/daikinmadoka/internal/MadokaMessageTest.java diff --git a/CODEOWNERS b/CODEOWNERS index 2fb763c319f9a..0d66331a9d6fb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -24,6 +24,7 @@ /bundles/org.openhab.binding.bluetooth.bluegiga/ @cdjackson @kaikreuzer /bundles/org.openhab.binding.bluetooth.bluez/ @cdjackson @kaikreuzer /bundles/org.openhab.binding.bluetooth.blukii/ @kaikreuzer +/bundles/org.openhab.binding.bluetooth.daikinmadoka/ @blafois /bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen /bundles/org.openhab.binding.boschindego/ @jofleck /bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index c7b20c2a3e5ec..54e1f2e6cdf73 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -109,6 +109,11 @@ org.openhab.binding.bluetooth.blukii ${project.version}
+ + org.openhab.addons.bundles + org.openhab.binding.bluetooth.daikinmadoka + ${project.version} + org.openhab.addons.bundles org.openhab.binding.bluetooth.ruuvitag diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/.classpath b/bundles/org.openhab.binding.bluetooth.daikinmadoka/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/.project b/bundles/org.openhab.binding.bluetooth.daikinmadoka/.project new file mode 100644 index 0000000000000..87f9472b85645 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.bluetooth.daikinmadoka + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/NOTICE b/bundles/org.openhab.binding.bluetooth.daikinmadoka/NOTICE new file mode 100644 index 0000000000000..4ce688fce1e40 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-core diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/README.md b/bundles/org.openhab.binding.bluetooth.daikinmadoka/README.md new file mode 100644 index 0000000000000..0a18ef8940e6e --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/README.md @@ -0,0 +1,110 @@ +# DaikinMadoka + +This extension implements communication with Daikin Madoka BRC1H thermostat over Bluetooth Low Energy (BLE) communication protocol. +The device uses an UART over BLE serial communication protocol over BLE WriteWithoutResponse and Notify characteristics. + +[BRC1H on Daikin website (FR)](https://www.daikin.fr/fr_fr/famille-produits/Systemes-commande-intelligents/BRC1H.html) + +[BRC1H on Daikin website (EN)](https://www.daikin.eu/en_us/product-group/control-systems/BRC1H.html) + + +## Supported Things + + +| Thing Type ID | Description | +| ------------- | ----------- | +| brc1h | BRC1H BLE Thermostat | + + +## Discovery + +As a pairing of the Thermostat is necessary (Bluetooth), no automatic discovery is implemented. + +## Thing Configuration + +* address: The Bluetooth MAC Address of the BRC1H controller + +Example with a DBusBlueZ Bluetooth Bridge: + +``` + +Bridge bluetooth:dbusbluez:hci0 [ address="00:1A:7D:DA:71:13" ] + +Thing bluetooth:brc1h:hci0:salon (bluetooth:dbusbluez:hci0) [ address="00:CC:3F:B2:80:CA" ] + +``` + + +## Channels + +_Here you should provide information about available channel types, what their meaning is and how they can be used._ + +_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/ESH-INF/thing``` of your binding._ + +| Channel Type ID | Item Type | Access Mode | Description | +|----------|--------|-----------|-------------------| +| onOffStatus | Switch | R/W |Switches On or Off the unit | +| indoorTemperature | Number:Temperature | R | Indoor temperature from the Thermostat +| outdoorTemperature | Number:Temperature | R | Outdoor temperature from the external unit. Not always supported/reported. +| commCtrlVersion | String | R | Communication Controller Firmware Version +| remoteCtrlVersion | String | R | Remote Controller Firmware Version +| operationMode | String | R/W | The operation mode of the AC unit. Currently supported values: HEAT, COOL. +| fanSpeed | Number | R/W | This is a "virtual channel" : its value is calculated depending on current operation mode. It is the channel to be used to change the fan speed, whatever the current mode is. Fan speed are from 1 to 5. On BRC1H, the device supports 3 speeds: LOW (1), MEDIUM (2-4), MAX (5). +| setpoint | Number:Temperature | R/W | This is a "virtual channel" : its value is calculated depending on current operation mode. It is the channel to be used to change the setpoint, whatever the current mode is. +| homekitCurrentHeatingCoolingMode | String | R | This channel is a "virtual channel" to be used with the HomeKit add-on to implement Thermostat thing. Values supported are the HomeKit addon ones: Off, CoolOn, HeatOn, Auto. +| homekitTargetHeatingCoolingMode | String | R/W | This channel is a "virtual channel" to be used with the HomeKit add-on to implement Thermostat thing. Values supported are the HomeKit addon ones: Off, CoolOn, HeatOn, Auto. + +## Full Example + +### daikinmadoka.things: + +``` + +Bridge bluetooth:dbusbluez:hci0 [ address="00:1A:7D:DA:71:13" ] + +Thing bluetooth:brc1h:hci0:salon (bluetooth:dbusbluez:hci0) [ address="00:CC:3F:B2:80:CA" ] + +``` + +### daikinmadoka.items: + +``` + +Group g_climSalon "Salon" [ "Thermostat" ] + +Switch climSalon_onOff "Climatisation Salon" (g_climSalon) { channel="bluetooth:brc1h:hci0:salon:onOffStatus" } +Number climSalon_indoorTemperature "Température Intérieure" (g_climSalon) [ "CurrentTemperature" ] { channel="bluetooth:brc1h:hci0:salon:indoorTemperature" } +Number climSalon_outdoorTemperature "Température Extérieure" (g_climSalon) { channel="bluetooth:brc1h:hci0:salon:outdoorTemperature" } + +String climSalon_commCtrlVersion (g_climSalon) { channel="bluetooth:brc1h:hci0:salon:commCtrlVersion" } +String climSalon_remoteCtrlVersion (g_climSalon) { channel="bluetooth:brc1h:hci0:salon:remoteCtrlVersion" } + +Number climSalon_fanSpeed (g_climSalon) { channel="bluetooth:brc1h:hci0:salon:fanSpeed" } + +Number climSalon_setpoint (g_climSalon) [ "homekit:TargetTemperature" ] { channel="bluetooth:brc1h:hci0:salon:setpoint" } + +String climSalon_operationMode (g_climSalon) { channel="bluetooth:brc1h:hci0:salon:operationMode" } + +String climSalon_CurrentHeatingCoolingMode (g_climSalon) [ "homekit:CurrentHeatingCoolingMode" ] { channel="bluetooth:brc1h:hci0:salon:homekitCurrentHeatingCoolingMode" } +String climSalon_TargetHeatingCoolingMode (g_climSalon) [ "homekit:TargetHeatingCoolingMode" ] { channel="bluetooth:brc1h:hci0:salon:homekitTargetHeatingCoolingMode" } + +``` + +## Pairing the BRC1H + +The Daikin Madoka BRC1H Thermostat requires Bluetooth Pairing before it can be used. +This pairing process can be a bit challenging, as it seems the timing is very important for it success. + +We suggest that the Bluetooth adapter is not being used by another component during the pairing phase. +As such, if you have other Bluetooth Things in your OpenHAB, it is suggested to stop the openhab service before doing the pairing. + + * Ensure that your BRC1H has Bluetooth enabled in the menu + * Open `bluetoothctl` on your openHAB server - preferably as `root` + * start scanning by typing `scan on` + * After few seconds, stop scanning `scan off` + * Start the pairing process by typing `pair ` + * On the BRC1H, confirm the pairing request, and quickly confirm as well on your server by typing `yes` + +A successful pairing ends with `pairing successful`. + +For more information on pairing a device in command line on Linux, refer to [official documentation](https://docs.ubuntu.com/core/en/stacks/bluetooth/bluez/docs/reference/pairing/outbound). diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/pom.xml b/bundles/org.openhab.binding.bluetooth.daikinmadoka/pom.xml new file mode 100644 index 0000000000000..12228de0b2146 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.7-SNAPSHOT + + + org.openhab.binding.bluetooth.daikinmadoka + + openHAB Add-ons :: Bundles :: DaikinMadoka Bluetooth Adapter + + + + org.openhab.addons.bundles + org.openhab.binding.bluetooth + ${project.version} + provided + + + + diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/feature/feature.xml b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/feature/feature.xml new file mode 100644 index 0000000000000..22a237ae7b6e8 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version} + mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.daikinmadoka/${project.version} + + diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/DaikinMadokaBindingConstants.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/DaikinMadokaBindingConstants.java new file mode 100644 index 0000000000000..b193033376295 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/DaikinMadokaBindingConstants.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka; + +import java.util.UUID; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.openhab.binding.bluetooth.BluetoothBindingConstants; + +/** + * The {@link DaikinMadokaBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Benjamin Lafois - Initial contribution + */ +@NonNullByDefault +public class DaikinMadokaBindingConstants { + + private DaikinMadokaBindingConstants() { + } + + public static final ThingTypeUID THING_TYPE_BRC1H = new ThingTypeUID(BluetoothBindingConstants.BINDING_ID, "brc1h"); + + public static final String CHANNEL_ID_ONOFF_STATUS = "onOffStatus"; + public static final String CHANNEL_ID_INDOOR_TEMPERATURE = "indoorTemperature"; + public static final String CHANNEL_ID_OUTDOOR_TEMPERATURE = "outdoorTemperature"; + public static final String CHANNEL_ID_COMMUNICATION_CONTROLLER_VERSION = "commCtrlVersion"; + public static final String CHANNEL_ID_REMOTE_CONTROLLER_VERSION = "remoteCtrlVersion"; + + public static final String CHANNEL_ID_OPERATION_MODE = "operationMode"; + public static final String CHANNEL_ID_FAN_SPEED = "fanSpeed"; + public static final String CHANNEL_ID_SETPOINT = "setpoint"; + public static final String CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE = "homekitCurrentHeatingCoolingMode"; + public static final String CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE = "homekitTargetHeatingCoolingMode"; + public static final String CHANNEL_ID_HOMEBRIDGE_MODE = "homebridgeMode"; + + /** + * BLUETOOTH UUID (service + chars) + */ + public static final UUID SERVICE_UART_UUID = UUID.fromString("2141E110-213A-11E6-B67B-9E71128CAE77"); + public static final UUID CHAR_WRITE_WITHOUT_RESPONSE_UUID = UUID.fromString("2141E112-213A-11E6-B67B-9E71128CAE77"); + public static final UUID CHAR_NOTIF_UUID = UUID.fromString("2141E111-213A-11E6-B67B-9E71128CAE77"); + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java new file mode 100644 index 0000000000000..ab6ca9ad1d30a --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java @@ -0,0 +1,772 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.handler; + +import java.util.Arrays; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.common.NamedThreadFactory; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.eclipse.smarthome.core.util.HexUtils; +import org.openhab.binding.bluetooth.BluetoothCharacteristic; +import org.openhab.binding.bluetooth.BluetoothCompletionStatus; +import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState; +import org.openhab.binding.bluetooth.ConnectedBluetoothHandler; +import org.openhab.binding.bluetooth.daikinmadoka.DaikinMadokaBindingConstants; +import org.openhab.binding.bluetooth.daikinmadoka.internal.BRC1HUartProcessor; +import org.openhab.binding.bluetooth.daikinmadoka.internal.DaikinMadokaConfiguration; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.FanSpeed; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.OperationMode; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaSettings; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.BRC1HCommand; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetFanspeedCommand; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetIndoorOutoorTemperatures; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetOperationmodeCommand; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetPowerstateCommand; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetSetpointCommand; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetVersionCommand; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.ResponseListener; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetFanspeedCommand; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetOperationmodeCommand; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetPowerstateCommand; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetSetpointCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link DaikinMadokaHandler} is responsible for handling commands, which are + * sent to one of the channels as well as updating channel values. + * + * @author Benjamin Lafois - Initial contribution + */ +@NonNullByDefault +public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements ResponseListener { + + private final Logger logger = LoggerFactory.getLogger(DaikinMadokaHandler.class); + + private @Nullable DaikinMadokaConfiguration config; + + private @Nullable ExecutorService commandExecutor; + + private @Nullable ScheduledFuture refreshJob; + + // UART Processor is in charge of reassembling chunks + private BRC1HUartProcessor uartProcessor = new BRC1HUartProcessor(this); + + private volatile @Nullable BRC1HCommand currentCommand = null; + + private MadokaSettings madokaSettings = new MadokaSettings(); + + public DaikinMadokaHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + super.initialize(); + + logger.debug("[{}] Start initializing!", super.thing.getUID().getId()); + + // Load Configuration + config = getConfigAs(DaikinMadokaConfiguration.class); + DaikinMadokaConfiguration c = config; + + logger.debug("[{}] Parameter value [refreshInterval]: {}", super.thing.getUID().getId(), c.refreshInterval); + logger.debug("[{}] Parameter value [commandTimeout]: {}", super.thing.getUID().getId(), c.commandTimeout); + + if (getBridge() == null) { + logger.debug("[{}] Bridge is null. Exiting.", super.thing.getUID().getId()); + return; + } + + this.commandExecutor = Executors + .newSingleThreadExecutor(new NamedThreadFactory(thing.getUID().getAsString(), true)); + + this.refreshJob = scheduler.scheduleWithFixedDelay(() -> { + // It is useless to refresh version all the time ! Just once. + if (this.madokaSettings.getCommunicationControllerVersion() == null + || this.madokaSettings.getRemoteControllerVersion() == null) { + submitCommand(new GetVersionCommand()); + } + submitCommand(new GetIndoorOutoorTemperatures()); + submitCommand(new GetOperationmodeCommand()); + submitCommand(new GetPowerstateCommand()); // always keep the "GetPowerState" aftern the "GetOperationMode" + submitCommand(new GetSetpointCommand()); + submitCommand(new GetFanspeedCommand()); + }, 10, c.refreshInterval, TimeUnit.SECONDS); + } + + @Override + public void dispose() { + logger.debug("[{}] dispose()", super.thing.getUID().getId()); + + dispose(refreshJob); + dispose(commandExecutor); + dispose(currentCommand); + + // Unsubscribe to characteristic notifications + if (this.device != null) { + BluetoothCharacteristic charNotif = this.device + .getCharacteristic(DaikinMadokaBindingConstants.CHAR_NOTIF_UUID); + + if (charNotif != null) { + @NonNull + BluetoothCharacteristic c = charNotif; + this.device.disableNotifications(c); + } + } + + super.dispose(); + } + + private static void dispose(@Nullable ExecutorService executor) { + if (executor != null) { + executor.shutdownNow(); + } + } + + private static void dispose(@Nullable ScheduledFuture future) { + if (future != null) { + future.cancel(true); + } + } + + private static void dispose(@Nullable BRC1HCommand command) { + if (command != null) { + // even if it already completed it doesn't really matter. + // on the off chance that the commandExecutor is waiting on the command, we can wake it up and cause it to + // terminate + command.setState(BRC1HCommand.State.FAILED); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("[{}] Channel: {}, Command: {}", super.thing.getUID().getId(), channelUID, command); + + if (command instanceof RefreshType) { + // The refresh commands are not supported in query mode. + // The binding will notify updates on channels + return; + } + + switch (channelUID.getId()) { + case DaikinMadokaBindingConstants.CHANNEL_ID_SETPOINT: + try { + QuantityType setpoint = (QuantityType) command; + DecimalType dt = new DecimalType(setpoint.intValue()); + submitCommand(new SetSetpointCommand(dt, dt)); + } catch (Exception e) { + logger.warn("Data received is not a valid temperature.", e); + } + break; + case DaikinMadokaBindingConstants.CHANNEL_ID_ONOFF_STATUS: + try { + OnOffType oot = (OnOffType) command; + submitCommand(new SetPowerstateCommand(oot)); + } catch (Exception e) { + logger.warn("Data received is not a valid on/off status", e); + } + break; + case DaikinMadokaBindingConstants.CHANNEL_ID_FAN_SPEED: + try { + DecimalType fanSpeed = (DecimalType) command; + FanSpeed fs = FanSpeed.valueOf(fanSpeed.intValue()); + submitCommand(new SetFanspeedCommand(fs, fs)); + } catch (Exception e) { + logger.warn("Data received is not a valid FanSpeed status", e); + } + break; + case DaikinMadokaBindingConstants.CHANNEL_ID_OPERATION_MODE: + try { + StringType operationMode = (StringType) command; + OperationMode m = OperationMode.valueOf(operationMode.toFullString()); + + submitCommand(new SetOperationmodeCommand(m)); + } catch (Exception e) { + logger.warn("Data received is not a valid OPERATION MODE", e); + } + break; + case DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE: + try { + // Homebridge are discrete value different from Daikin + // 0 - Off + // 1 - Heating + // 2 - Cooling + // 3 - Auto + DecimalType homebridgeMode = (DecimalType) command; + switch (homebridgeMode.intValue()) { + case 0: // Off + submitCommand(new SetPowerstateCommand(OnOffType.OFF)); + break; + case 1: // Heating + submitCommand(new SetOperationmodeCommand(OperationMode.HEAT)); + if (madokaSettings.getOnOffState() == OnOffType.OFF) { + submitCommand(new SetPowerstateCommand(OnOffType.ON)); + } + break; + case 2: // Cooling + submitCommand(new SetOperationmodeCommand(OperationMode.COOL)); + if (madokaSettings.getOnOffState() == OnOffType.OFF) { + submitCommand(new SetPowerstateCommand(OnOffType.ON)); + } + break; + case 3: // Auto + submitCommand(new SetOperationmodeCommand(OperationMode.AUTO)); + if (madokaSettings.getOnOffState() == OnOffType.OFF) { + submitCommand(new SetPowerstateCommand(OnOffType.ON)); + } + break; + default: // Invalid Value - in case of new FW + logger.warn("Invalid value received for channel {}. Ignoring.", channelUID); + break; + } + } catch (Exception e) { + logger.warn("Data received is not a valid HOMEBRIDGE OPERATION MODE", e); + } + break; + case DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE: + try { + StringType homekitOperationMode = (StringType) command; + + switch (homekitOperationMode.toString()) { + case "Off": + submitCommand(new SetPowerstateCommand(OnOffType.OFF)); + break; + case "CoolOn": + submitCommand(new SetOperationmodeCommand(OperationMode.COOL)); + if (madokaSettings.getOnOffState() == OnOffType.OFF) { + submitCommand(new SetPowerstateCommand(OnOffType.ON)); + } + break; + case "HeatOn": + submitCommand(new SetOperationmodeCommand(OperationMode.HEAT)); + if (madokaSettings.getOnOffState() == OnOffType.OFF) { + submitCommand(new SetPowerstateCommand(OnOffType.ON)); + } + break; + case "Auto": + submitCommand(new SetOperationmodeCommand(OperationMode.AUTO)); + if (madokaSettings.getOnOffState() == OnOffType.OFF) { + submitCommand(new SetPowerstateCommand(OnOffType.ON)); + } + break; + default: + break; + } + } catch (Exception e) { + logger.info("Error while setting mode through HomeKIt received Mode"); + } + default: + break; + } + + } + + @Override + public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) { + super.onCharacteristicUpdate(characteristic); + + // Check that arguments are valid. + if (characteristic.getUuid() == null) { + return; + } + + // We are only interested in the Notify Characteristic of UART service + if (!characteristic.getUuid().equals(DaikinMadokaBindingConstants.CHAR_NOTIF_UUID)) { + return; + } + + // A message cannot be null or have a 0-byte length + byte[] msgBytes = characteristic.getByteValue(); + if (msgBytes == null || msgBytes.length == 0) { + return; + } + + this.uartProcessor.chunkReceived(msgBytes); + } + + private void submitCommand(BRC1HCommand command) { + Executor executor = commandExecutor; + + if (executor != null) { + executor.execute(() -> processCommand(command)); + } + } + + private void processCommand(BRC1HCommand command) { + logger.debug("[{}] ProcessCommand {}", super.thing.getUID().getId(), command.getClass().getSimpleName()); + + try { + currentCommand = command; + uartProcessor.abandon(); + + if (device == null || device.getConnectionState() != ConnectionState.CONNECTED) { + logger.debug("Unable to send command {} to device {}: not connected", + command.getClass().getSimpleName(), address); + command.setState(BRC1HCommand.State.FAILED); + return; + } + + if (!resolved) { + logger.debug("Unable to send command {} to device {}: services not resolved", + command.getClass().getSimpleName(), device.getAddress()); + command.setState(BRC1HCommand.State.FAILED); + return; + } + + BluetoothCharacteristic charWrite = device + .getCharacteristic(DaikinMadokaBindingConstants.CHAR_WRITE_WITHOUT_RESPONSE_UUID); + if (charWrite == null) { + logger.warn("Unable to execute {}. Characteristic '{}' could not be found.", + command.getClass().getSimpleName(), + DaikinMadokaBindingConstants.CHAR_WRITE_WITHOUT_RESPONSE_UUID); + command.setState(BRC1HCommand.State.FAILED); + return; + } + + BluetoothCharacteristic charNotif = this.device + .getCharacteristic(DaikinMadokaBindingConstants.CHAR_NOTIF_UUID); + + if (charNotif != null) { + device.enableNotifications(charNotif); + } + + charWrite.setValue(command.getRequest()); + command.setState(BRC1HCommand.State.ENQUEUED); + device.writeCharacteristic(charWrite); + + if (this.config != null) { + if (!command.awaitStateChange(this.config.commandTimeout, TimeUnit.MILLISECONDS, + BRC1HCommand.State.SUCCEEDED, BRC1HCommand.State.FAILED)) { + logger.debug("Command {} to device {} timed out", command, device.getAddress()); + command.setState(BRC1HCommand.State.FAILED); + } + } + } catch (Exception e) { + currentCommand = null; + // Let the exception bubble the stack! + throw new RuntimeException(e); + } + + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic, + BluetoothCompletionStatus status) { + super.onCharacteristicWriteComplete(characteristic, status); + + byte[] request = characteristic.getByteValue(); + BRC1HCommand command = currentCommand; + + if (command != null) { + if (!Arrays.equals(request, command.getRequest())) { + logger.debug("Write completed for unknown command"); + return; + } + switch (status) { + case SUCCESS: + command.setState(BRC1HCommand.State.SENT); + break; + case ERROR: + command.setState(BRC1HCommand.State.FAILED); + break; + } + } else { + if (logger.isDebugEnabled()) { + logger.debug("No command found that matches request {}", HexUtils.bytesToHex(request)); + } + } + } + + /** + * When the method is triggered, it means that all message chunks have been received, re-assembled in the right + * order and that the payload is ready to be processed. + */ + @Override + public void receivedResponse(byte[] response) { + logger.debug("Received Response"); + BRC1HCommand command = currentCommand; + + if (command == null) { + if (logger.isDebugEnabled()) { + logger.debug("No command present to handle response {}", HexUtils.bytesToHex(response)); + } + } else { + try { + command.handleResponse(scheduler, this, MadokaMessage.parse(response)); + } catch (MadokaParsingException e) { + logger.debug("Response message could not be parsed correctly ({}): {}. Reason: {}", + command.getClass().getSimpleName(), HexUtils.bytesToHex(response), e.getMessage()); + } + } + } + + @Override + public void receivedResponse(GetVersionCommand command) { + String commCtrlVers = command.getCommunicationControllerVersion(); + if (commCtrlVers != null) { + this.madokaSettings.setCommunicationControllerVersion(commCtrlVers); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_COMMUNICATION_CONTROLLER_VERSION, + new StringType(commCtrlVers)); + } + + String remoteCtrlVers = command.getRemoteControllerVersion(); + if (remoteCtrlVers != null) { + this.madokaSettings.setRemoteControllerVersion(remoteCtrlVers); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_REMOTE_CONTROLLER_VERSION, + new StringType(remoteCtrlVers)); + } + } + + @Override + public void receivedResponse(GetFanspeedCommand command) { + if (command.getCoolingFanSpeed() == null || command.getHeatingFanSpeed() == null) { + return; + } + + // We need the current operation mode to determine which Fan Speed we use (cooling or heating) + OperationMode operationMode = this.madokaSettings.getOperationMode(); + if (operationMode == null) { + return; + } + + FanSpeed fs; + + switch (operationMode) { + case AUTO: + logger.debug("In AutoMode, CoolingFanSpeed = {}, HeatingFanSpeed = {}", command.getCoolingFanSpeed(), + command.getHeatingFanSpeed()); + fs = command.getHeatingFanSpeed(); + break; + case HEAT: + fs = command.getHeatingFanSpeed(); + break; + case COOL: + fs = command.getCoolingFanSpeed(); + break; + default: + return; + } + + if (fs == null) { + return; + } + + // No need to re-set if it is the same value + if (fs.equals(this.madokaSettings.getFanspeed())) { + return; + } + + this.madokaSettings.setFanspeed(fs); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_FAN_SPEED, new DecimalType(fs.value())); + } + + @Override + public void receivedResponse(GetSetpointCommand command) { + if (command.getCoolingSetpoint() == null || command.getHeatingSetpoint() == null) { + return; + } + + // We need the current operation mode to determine which Fan Speed we use (cooling or heating) + OperationMode operationMode = this.madokaSettings.getOperationMode(); + if (operationMode == null) { + return; + } + + DecimalType sp; + + switch (operationMode) { + case AUTO: + logger.debug("In AutoMode, CoolingSetpoint = {}, HeatingSetpoint = {}", command.getCoolingSetpoint(), + command.getHeatingSetpoint()); + sp = command.getHeatingSetpoint(); + break; + case HEAT: + sp = command.getHeatingSetpoint(); + break; + case COOL: + sp = command.getCoolingSetpoint(); + break; + default: + return; + } + + if (sp == null) { + return; + } + + // No need to re-set if it is the same value + if (sp.equals(this.madokaSettings.getSetpoint())) { + return; + } + + this.madokaSettings.setSetpoint(sp); + + DecimalType dt = this.madokaSettings.getSetpoint(); + if (dt != null) { + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_SETPOINT, dt); + } + } + + @Override + public void receivedResponse(GetOperationmodeCommand command) { + if (command.getOperationMode() == null) { + logger.debug("OperationMode is null."); + return; + } + + OperationMode newMode = command.getOperationMode(); + + if (newMode == null) { + return; + } + + this.madokaSettings.setOperationMode(newMode); + + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OPERATION_MODE, new StringType(newMode.name())); + + // For HomeKit channel, we need to map it to HomeKit supported strings + OnOffType ooStatus = madokaSettings.getOnOffState(); + + if (ooStatus != null && ooStatus == OnOffType.ON) { + switch (newMode) { + case COOL: + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE, + new StringType("Cooling")); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(2)); + break; + case HEAT: + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE, + new StringType("Heating")); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(1)); + break; + case AUTO: + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE, + new StringType("Auto")); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(3)); + default: + break; + } + } + + // If this is the first channel update - then we set target = current mode + if (this.madokaSettings.getHomekitTargetMode() == null) { + String newHomekitTargetStatus = null; + + // For HomeKit channel, we need to map it to HomeKit supported strings + switch (newMode) { + case COOL: + newHomekitTargetStatus = "CoolOn"; + break; + case HEAT: + newHomekitTargetStatus = "HeatOn"; + break; + default: + return; + } + + if (ooStatus != null && ooStatus == OnOffType.ON) { + this.madokaSettings.setHomekitTargetMode(newHomekitTargetStatus); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE, + new StringType(newHomekitTargetStatus)); + } else if (ooStatus != null && ooStatus == OnOffType.OFF) { + newHomekitTargetStatus = "Off"; + this.madokaSettings.setHomekitTargetMode(newHomekitTargetStatus); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE, + new StringType(newHomekitTargetStatus)); + } + + } + } + + @Override + public void receivedResponse(GetPowerstateCommand command) { + if (command.isPowerState() == null) { + return; + } + + OnOffType oot = command.isPowerState() ? OnOffType.ON : OnOffType.OFF; + + this.madokaSettings.setOnOffState(oot); + + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_ONOFF_STATUS, oot); + + if (oot.equals(OnOffType.OFF)) { + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE, + new StringType("Off")); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE, + new StringType("Off")); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(0)); + } + } + + @Override + public void receivedResponse(GetIndoorOutoorTemperatures command) { + DecimalType newIndoorTemp = command.getIndoorTemperature(); + if (newIndoorTemp != null) { + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_TEMPERATURE, newIndoorTemp); + this.madokaSettings.setIndoorTemperature(newIndoorTemp); + } + + DecimalType newOutdoorTemp = command.getOutdoorTemperature(); + if (newOutdoorTemp == null) { + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OUTDOOR_TEMPERATURE, UnDefType.UNDEF); + } else { + this.madokaSettings.setOutdoorTemperature(newOutdoorTemp); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OUTDOOR_TEMPERATURE, newOutdoorTemp); + } + } + + @Override + public void receivedResponse(SetPowerstateCommand command) { + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_ONOFF_STATUS, command.getPowerState()); + + madokaSettings.setOnOffState(command.getPowerState()); + + if (command.getPowerState() == OnOffType.ON) { + // Depending on the state + + OperationMode operationMode = madokaSettings.getOperationMode(); + if (operationMode == null) { + return; + } + + switch (operationMode) { + case AUTO: + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE, + new StringType("Auto")); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(3)); + break; + case HEAT: + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE, + new StringType("Heating")); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(1)); + break; + case COOL: + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE, + new StringType("Cooling")); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(2)); + break; + default: // Other Modes are not [yet] supported + break; + } + } else { + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE, + new StringType("Off")); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(0)); + } + } + + @Override + public void receivedResponse(SetSetpointCommand command) { + // The update depends on the mode - so if not set - skip + OperationMode operationMode = this.madokaSettings.getOperationMode(); + if (operationMode == null) { + return; + } + + switch (operationMode) { + case HEAT: + this.madokaSettings.setSetpoint(command.getHeatingSetpoint()); + break; + case COOL: + this.madokaSettings.setSetpoint(command.getCoolingSetpoint()); + break; + case AUTO: + // Here we don't really care if we are taking cooling or heating... + this.madokaSettings.setSetpoint(command.getCoolingSetpoint()); + break; + default: + return; + } + + DecimalType dt = madokaSettings.getSetpoint(); + if (dt != null) { + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_SETPOINT, dt); + } + } + + /** + * Received response to "SetOperationmodeCommand" command + */ + @Override + public void receivedResponse(SetOperationmodeCommand command) { + this.madokaSettings.setOperationMode(command.getOperationMode()); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OPERATION_MODE, + new StringType(command.getOperationMode().toString())); + } + + /** + * Received response to "SetFanSpeed" command + */ + @Override + public void receivedResponse(SetFanspeedCommand command) { + // The update depends on the mode - so if not set - skip + OperationMode operationMode = this.madokaSettings.getOperationMode(); + if (operationMode == null) { + return; + } + + FanSpeed fanSpeed; + switch (operationMode) { + case HEAT: + fanSpeed = command.getHeatingFanSpeed(); + this.madokaSettings.setFanspeed(fanSpeed); + break; + case COOL: + fanSpeed = command.getCoolingFanSpeed(); + this.madokaSettings.setFanspeed(fanSpeed); + break; + case AUTO: + fanSpeed = command.getCoolingFanSpeed(); // Arbitrary cooling or heating... They are the same! + this.madokaSettings.setFanspeed(fanSpeed); + break; + default: + return; + } + + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_FAN_SPEED, new DecimalType(fanSpeed.value())); + } + + private void updateStateIfLinked(String channelId, State state) { + ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelId); + if (isLinked(channelUID)) { + updateState(channelUID, state); + } + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/BRC1HUartProcessor.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/BRC1HUartProcessor.java new file mode 100644 index 0000000000000..728a4093ec6ca --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/BRC1HUartProcessor.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal; + +import java.io.ByteArrayOutputStream; +import java.util.Comparator; +import java.util.concurrent.ConcurrentSkipListSet; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.ResponseListener; + +/** + * As the protocol emutes an UART communication over BLE (characteristics write/notify), this class takes care of BLE + * transport. + * + * @author Benjamin Lafois - Initial contribution + */ +@NonNullByDefault +public class BRC1HUartProcessor { + + /** + * Maximum number of bytes per message chunk, including headers + */ + public static final int MAX_CHUNK_SIZE = 20; + + /** + * In the unlikely event of messages arrive in wrong order, this comparator will sort the queue + */ + private Comparator chunkSorter = (byte[] m1, byte[] m2) -> m1[0] - m2[0]; + + private ConcurrentSkipListSet uartMessages = new ConcurrentSkipListSet<>(chunkSorter); + + private ResponseListener responseListener; + + public BRC1HUartProcessor(ResponseListener responseListener) { + this.responseListener = responseListener; + } + + private boolean isMessageComplete() { + int messagesInQueue = this.uartMessages.size(); + + if (messagesInQueue <= 0) { + return false; + } + + byte[] firstMessageInQueue = uartMessages.first(); + if (firstMessageInQueue.length < 2) { + return false; + } + + int expectedChunks = (int) Math.ceil(firstMessageInQueue[1] / (MAX_CHUNK_SIZE - 1.0)); + if (expectedChunks != messagesInQueue) { + return false; + } + + // Check that we have every single ID + int expected = 0; + for (byte[] m : this.uartMessages) { + if (m.length < 2) { + return false; + } + + if (m[0] != expected++) { + return false; + } + } + return true; + } + + public void chunkReceived(byte[] byteValue) { + this.uartMessages.add(byteValue); + if (isMessageComplete()) { + + // Beyond this point, full message received + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + for (byte[] msg : uartMessages) { + if (msg.length > 1) { + bos.write(msg, 1, msg.length - 1); + } + } + + this.uartMessages.clear(); + + this.responseListener.receivedResponse(bos.toByteArray()); + } + } + + public void abandon() { + this.uartMessages.clear(); + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/DaikinMadokaConfiguration.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/DaikinMadokaConfiguration.java new file mode 100644 index 0000000000000..d961f54eb0f9e --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/DaikinMadokaConfiguration.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal; + +/** + * The {@link DaikinMadokaConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Benjamin Lafois - Initial contribution + */ +public class DaikinMadokaConfiguration { + + public String address; + public Integer refreshInterval; + public Integer commandTimeout; + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/DaikinMadokaHandlerFactory.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/DaikinMadokaHandlerFactory.java new file mode 100644 index 0000000000000..2861163f2d9f3 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/DaikinMadokaHandlerFactory.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal; + +import static org.openhab.binding.bluetooth.daikinmadoka.DaikinMadokaBindingConstants.THING_TYPE_BRC1H; + +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.openhab.binding.bluetooth.daikinmadoka.handler.DaikinMadokaHandler; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link DaikinMadokaHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Benjamin Lafois - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.bluetooth.daikinmadoka", service = ThingHandlerFactory.class) +public class DaikinMadokaHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_BRC1H); + + private final Logger logger = LoggerFactory.getLogger(DaikinMadokaHandlerFactory.class); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + logger.debug("Request to create handler for thing {}", thing.getThingTypeUID()); + + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_BRC1H.equals(thingTypeUID)) { + logger.debug("Thing is matching BRC1H"); + + return new DaikinMadokaHandler(thing); + } + + return null; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaMessage.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaMessage.java new file mode 100644 index 0000000000000..437b2396ee629 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaMessage.java @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.BRC1HCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class represents a message transmitted or received from the BRC1H controller as a serial protocol + * + * @author Benjamin Lafois - Initial contribution + */ +@NonNullByDefault +public class MadokaMessage { + + private static final Logger logger = LoggerFactory.getLogger(MadokaMessage.class); + + private int messageId; + private final Map values; + + private byte @Nullable [] rawMessage; + + private MadokaMessage() { + values = new HashMap<>(); + } + + public static byte[] createRequest(BRC1HCommand command, MadokaValue... parameters) { + try { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + DataOutputStream request = new DataOutputStream(output); + + // Message Length - Computed in the end + request.writeByte(0); + request.writeByte(0); + + // Command ID, coded on 3 bytes + request.writeByte(0); + request.writeShort(command.getCommandId()); + + if (parameters.length == 0) { + request.writeByte(0); + request.writeByte(0); + } else { + for (MadokaValue mv : parameters) { + request.writeByte(mv.getId()); + request.writeByte(mv.getSize()); + request.write(mv.getRawValue()); + } + } + + // Finally, compute array size + byte[] ret = output.toByteArray(); + ret[1] = (byte) (ret.length - 1); + + return ret; + } catch (IOException e) { + logger.info("Error while building request", e); + throw new RuntimeException(e); + } + } + + public static MadokaMessage parse(byte[] msg) throws MadokaParsingException { + // Msg format (bytes): + // ... + // So MINIMAL length is 4, to cover the message length + message ID + if (msg.length < 4) { + throw new MadokaParsingException("Message received is too short to be parsed."); + } + if (msg[0] != msg.length) { + throw new MadokaParsingException("Message size is not valid (different from byte[0])."); + } + + MadokaMessage m = new MadokaMessage(); + m.setRawMessage(msg); + m.messageId = ByteBuffer.wrap(msg, 2, 2).getShort(); + + MadokaValue mv = null; + + // Starting here, we are not on the safe side with previous msg.length check + for (int i = 4; i < msg.length;) { + if ((i + 1) >= msg.length) { + throw new MadokaParsingException("Truncated message detected while parsing response value header"); + } + + mv = new MadokaValue(); + mv.setId(msg[i]); + mv.setSize(Byte.toUnsignedInt(msg[i + 1])); + + if ((i + 1 + mv.getSize()) >= msg.length) { + throw new MadokaParsingException("Truncated message detected while parsing response value content"); + } + + mv.setRawValue(Arrays.copyOfRange(msg, i + 2, i + 2 + mv.getSize())); + + i += 2 + mv.getSize(); + + m.values.put(mv.getId(), mv); + } + + return m; + } + + private void setRawMessage(byte[] rawMessage) { + this.rawMessage = rawMessage; + } + + public byte @Nullable [] getRawMessage() { + return this.rawMessage; + } + + public int getMessageId() { + return messageId; + } + + public Map getValues() { + return values; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append(String.format("{ messageId: %d, values: [", this.messageId)); + + for (Map.Entry entry : values.entrySet()) { + sb.append(String.format(" { valueId: %d, valueSize: %d },", entry.getKey(), entry.getValue().getSize())); + } + + sb.append("] }"); + return sb.toString(); + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaParsingException.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaParsingException.java new file mode 100644 index 0000000000000..a83f0150170e1 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaParsingException.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This exception is thrown when an exception happens parsing a message from the BLE controller. + * + * @author Benjamin Lafois - Initial contribution + * + */ +@SuppressWarnings("serial") +@NonNullByDefault +public class MadokaParsingException extends Exception { + + public MadokaParsingException(String message) { + super(message); + } + + public MadokaParsingException(Throwable t) { + super(t); + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaProperties.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaProperties.java new file mode 100644 index 0000000000000..a5e91785dc0f9 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaProperties.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class contains the enums for the various modes supported by the BLE thermostat + * + * @author Benjamin Lafois - Initial contribution + * + */ +@NonNullByDefault +public class MadokaProperties { + + public enum FanSpeed { + MAX(5), + MEDIUM(3), + LOW(1); + + private int v; + + FanSpeed(int v) { + this.v = v; + } + + public static FanSpeed valueOf(int v) { + if (v == 5) { + return MAX; + } else if (v >= 2 && v <= 4) { + return MEDIUM; + } else { + return LOW; + } + } + + public int value() { + return v; + } + } + + public enum OperationMode { + FAN(0), + DRY(1), + AUTO(2), + COOL(3), + HEAT(4), + VENTILATION(5); + + private int v; + + OperationMode(int v) { + this.v = v; + } + + public static OperationMode valueOf(int v) { + for (OperationMode m : values()) { + if (m.v == v) { + return m; + } + } + // Should never happen + return HEAT; + } + + public int value() { + return v; + } + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaSettings.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaSettings.java new file mode 100644 index 0000000000000..f359a78eb0876 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaSettings.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.FanSpeed; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.OperationMode; + +/** + * This class contains the current state of the controllerw + * + * @author Benjamin Lafois - Initial contribution + * + */ +@NonNullByDefault +public class MadokaSettings { + + private @Nullable OnOffType onOffState; + + private @Nullable DecimalType setpoint; + + private @Nullable DecimalType indoorTemperature; + private @Nullable DecimalType outdoorTemperature; + + private @Nullable FanSpeed fanspeed; + + private @Nullable OperationMode operationMode; + + private @Nullable String homekitCurrentMode; + private @Nullable String homekitTargetMode; + + private @Nullable String communicationControllerVersion; + private @Nullable String remoteControllerVersion; + + public @Nullable OnOffType getOnOffState() { + return onOffState; + } + + public void setOnOffState(OnOffType onOffState) { + this.onOffState = onOffState; + } + + public @Nullable DecimalType getSetpoint() { + return setpoint; + } + + public void setSetpoint(DecimalType setpoint) { + this.setpoint = setpoint; + } + + public @Nullable DecimalType getIndoorTemperature() { + return indoorTemperature; + } + + public void setIndoorTemperature(DecimalType indoorTemperature) { + this.indoorTemperature = indoorTemperature; + } + + public @Nullable DecimalType getOutdoorTemperature() { + return outdoorTemperature; + } + + public void setOutdoorTemperature(DecimalType outdoorTemperature) { + this.outdoorTemperature = outdoorTemperature; + } + + public @Nullable FanSpeed getFanspeed() { + return fanspeed; + } + + public void setFanspeed(FanSpeed fanspeed) { + this.fanspeed = fanspeed; + } + + public @Nullable OperationMode getOperationMode() { + return operationMode; + } + + public void setOperationMode(OperationMode operationMode) { + this.operationMode = operationMode; + } + + public @Nullable String getHomekitCurrentMode() { + return homekitCurrentMode; + } + + public void setHomekitCurrentMode(String homekitCurrentMode) { + this.homekitCurrentMode = homekitCurrentMode; + } + + public @Nullable String getHomekitTargetMode() { + return homekitTargetMode; + } + + public void setHomekitTargetMode(String homekitTargetMode) { + this.homekitTargetMode = homekitTargetMode; + } + + public @Nullable String getCommunicationControllerVersion() { + return communicationControllerVersion; + } + + public void setCommunicationControllerVersion(String communicationControllerVersion) { + this.communicationControllerVersion = communicationControllerVersion; + } + + public @Nullable String getRemoteControllerVersion() { + return remoteControllerVersion; + } + + public void setRemoteControllerVersion(String remoteControllerVersion) { + this.remoteControllerVersion = remoteControllerVersion; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaValue.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaValue.java new file mode 100644 index 0000000000000..eee3c2c394b47 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/MadokaValue.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model; + +import java.nio.ByteBuffer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class is in charge of serializing/deserializing values from a message + * + * @author Benjamin Lafois - Initial contribution + */ +@NonNullByDefault +public class MadokaValue { + + private int id; + private int size; + private byte @Nullable [] rawValue; + + public MadokaValue(int id, int size, byte[] rawValue) { + this.id = id; + this.size = size; + this.rawValue = rawValue; + } + + public MadokaValue() { + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public byte @Nullable [] getRawValue() { + return rawValue; + } + + public void setRawValue(byte[] rawValue) { + this.rawValue = rawValue; + } + + public long getComputedValue() { + byte[] v = rawValue; + if (v != null) { + switch (size) { + case 1: + return v[0]; + case 2: + return ByteBuffer.wrap(v, 0, 2).getShort(); + case 4: + return ByteBuffer.wrap(v, 0, 4).getInt(); + default: + // unsupported + break; + } + } + return 0; + } +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/BRC1HCommand.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/BRC1HCommand.java new file mode 100644 index 0000000000000..db3f8dbb78fe2 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/BRC1HCommand.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands; + +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException; + +/** + * Abstract class for all BLE commands sent to the controller + * + * @author Benjamin Lafois - Initial contribution + * + */ +@NonNullByDefault +public abstract class BRC1HCommand { + + public enum State { + NEW, + ENQUEUED, + SENT, + SUCCEEDED, + FAILED + } + + private volatile State state = State.NEW; + + private final Lock stateLock = new ReentrantLock(); + + private final Condition stateCondition = stateLock.newCondition(); + + public abstract void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) + throws MadokaParsingException; + + /** + * THis command returns the message to be sent + * + * @return + */ + public abstract byte[] getRequest(); + + /** + * This is the command number, in the protocol + * + * @return + */ + public abstract int getCommandId(); + + /** + * Returns current state of the command. + * + * @return current state + */ + public State getState() { + return state; + } + + /** + * Sets state of the command. + * + * @param state new state + */ + public void setState(State state) { + stateLock.lock(); + try { + this.state = state; + stateCondition.signalAll(); + } finally { + stateLock.unlock(); + } + } + + public boolean awaitStateChange(long timeout, TimeUnit unit, State... expectedStates) throws InterruptedException { + stateLock.lock(); + try { + long nanosTimeout = unit.toNanos(timeout); + while (!isInAnyState(expectedStates)) { + if (nanosTimeout <= 0L) { + return false; + } + nanosTimeout = stateCondition.awaitNanos(nanosTimeout); + } + } finally { + stateLock.unlock(); + } + return true; + } + + private boolean isInAnyState(State[] acceptedStates) { + for (State acceptedState : acceptedStates) { + if (acceptedState == state) { + return true; + } + } + return false; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetFanspeedCommand.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetFanspeedCommand.java new file mode 100644 index 0000000000000..2eb12b00cdb63 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetFanspeedCommand.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands; + +import java.util.concurrent.Executor; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.FanSpeed; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Command used to set the FAN speed for all modes (auto/cool/heat...) + * + * @author Benjamin Lafois - Initial contribution + * + */ +@NonNullByDefault +public class GetFanspeedCommand extends BRC1HCommand { + + private final Logger logger = LoggerFactory.getLogger(GetFanspeedCommand.class); + + private @Nullable FanSpeed coolingFanSpeed; + private @Nullable FanSpeed heatingFanSpeed; + + @Override + public byte[] getRequest() { + return MadokaMessage.createRequest(this); + } + + @Override + public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) + throws MadokaParsingException { + + byte[] valueCoolingFanSpeed = mm.getValues().get(0x20).getRawValue(); + byte[] valueHeatingFanSpeed = mm.getValues().get(0x21).getRawValue(); + + if (valueCoolingFanSpeed == null || valueHeatingFanSpeed == null) { + setState(State.FAILED); + throw new MadokaParsingException("Incorrect cooling or heating fan speed value"); + } + + this.coolingFanSpeed = FanSpeed.valueOf(valueCoolingFanSpeed[0]); + this.heatingFanSpeed = FanSpeed.valueOf(valueHeatingFanSpeed[0]); + + logger.debug("coolingFanSpeed: {}", coolingFanSpeed); + logger.debug("heatingFanSpeed: {}", heatingFanSpeed); + + setState(State.SUCCEEDED); + executor.execute(() -> listener.receivedResponse(this)); + + } + + @Override + public int getCommandId() { + return 80; + } + + public @Nullable FanSpeed getCoolingFanSpeed() { + return coolingFanSpeed; + } + + public @Nullable FanSpeed getHeatingFanSpeed() { + return heatingFanSpeed; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetIndoorOutoorTemperatures.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetIndoorOutoorTemperatures.java new file mode 100644 index 0000000000000..f52d21bb684e2 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetIndoorOutoorTemperatures.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands; + +import java.util.concurrent.Executor; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This command returns the Indoor and Outdoor temperature. Outdoor is not always supported. + * + * @author Benjamin Lafois - Initial contribution + * + */ +@NonNullByDefault +public class GetIndoorOutoorTemperatures extends BRC1HCommand { + + private final Logger logger = LoggerFactory.getLogger(GetIndoorOutoorTemperatures.class); + + private @Nullable DecimalType indoorTemperature; + private @Nullable DecimalType outdoorTemperature; + + @Override + public byte[] getRequest() { + return MadokaMessage.createRequest(this); + } + + @Override + public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) + throws MadokaParsingException { + byte[] bIndoorTemperature = mm.getValues().get(0x40).getRawValue(); + byte[] bOutdoorTemperature = mm.getValues().get(0x41).getRawValue(); + + if (bIndoorTemperature == null || bOutdoorTemperature == null) { + setState(State.FAILED); + throw new MadokaParsingException("Incorrect indoor or outdoor temperature"); + } + + Integer iIndoorTemperature = Integer.valueOf(bIndoorTemperature[0]); + Integer iOutdoorTemperature = Integer.valueOf(bOutdoorTemperature[0]); + + if (iOutdoorTemperature == -1) { + iOutdoorTemperature = null; + } else { + if (iOutdoorTemperature < 0) { + iOutdoorTemperature = ((iOutdoorTemperature + 256) - 128) * -1; + } + } + + if (iIndoorTemperature != null) { + indoorTemperature = new DecimalType(iIndoorTemperature); + } + + if (iOutdoorTemperature != null) { + outdoorTemperature = new DecimalType(iOutdoorTemperature); + } + + logger.debug("Indoor Temp: {}", indoorTemperature); + logger.debug("Outdoor Temp: {}", outdoorTemperature); + + setState(State.SUCCEEDED); + executor.execute(() -> listener.receivedResponse(this)); + } + + public @Nullable DecimalType getIndoorTemperature() { + return indoorTemperature; + } + + public @Nullable DecimalType getOutdoorTemperature() { + return outdoorTemperature; + } + + @Override + public int getCommandId() { + return 272; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetOperationmodeCommand.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetOperationmodeCommand.java new file mode 100644 index 0000000000000..b1e4bf404abbc --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetOperationmodeCommand.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands; + +import java.util.concurrent.Executor; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.OperationMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This command returns the current AC operation mode + * + * @author Benjamin Lafois - Initial contribution + * + */ +@NonNullByDefault +public class GetOperationmodeCommand extends BRC1HCommand { + + private final Logger logger = LoggerFactory.getLogger(GetOperationmodeCommand.class); + + private @Nullable OperationMode operationMode; + + @Override + public byte[] getRequest() { + return MadokaMessage.createRequest(this); + } + + @Override + public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) + throws MadokaParsingException { + byte[] bOperationMode = mm.getValues().get(0x20).getRawValue(); + if (bOperationMode == null) { + setState(State.FAILED); + throw new MadokaParsingException("Incorrect operation mode"); + } + + operationMode = OperationMode.valueOf(bOperationMode[0]); + + logger.debug("operationMode: {}", operationMode); + + setState(State.SUCCEEDED); + executor.execute(() -> listener.receivedResponse(this)); + } + + @Override + public int getCommandId() { + return 48; + } + + public @Nullable OperationMode getOperationMode() { + return operationMode; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetPowerstateCommand.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetPowerstateCommand.java new file mode 100644 index 0000000000000..7ab26a50a1c49 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetPowerstateCommand.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands; + +import java.util.concurrent.Executor; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This command returns the current AC power state (on or off) + * + * @author Benjamin Lafois - Initial contribution + * + */ +@NonNullByDefault +public class GetPowerstateCommand extends BRC1HCommand { + + private final Logger logger = LoggerFactory.getLogger(GetPowerstateCommand.class); + + private @Nullable Boolean powerState; + + @Override + public byte[] getRequest() { + return MadokaMessage.createRequest(this); + } + + @Override + public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) + throws MadokaParsingException { + byte[] powerStateValue = mm.getValues().get(0x20).getRawValue(); + + if (powerStateValue == null || powerStateValue.length != 1) { + setState(State.FAILED); + throw new MadokaParsingException("Incorrect value for PowerState"); + } + + powerState = Integer.valueOf(powerStateValue[0]) == 1; + + logger.debug("PowerState: {}", powerState); + + setState(State.SUCCEEDED); + executor.execute(() -> listener.receivedResponse(this)); + } + + @Override + public int getCommandId() { + return 32; + } + + public @Nullable Boolean isPowerState() { + return powerState; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetSetpointCommand.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetSetpointCommand.java new file mode 100644 index 0000000000000..030d92e09e77f --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetSetpointCommand.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands; + +import java.util.concurrent.Executor; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This command returns the setpoint, whatever is the current mode. + * + * @author Benjamin Lafois - Initial contribution + * + */ +@NonNullByDefault +public class GetSetpointCommand extends BRC1HCommand { + + private final Logger logger = LoggerFactory.getLogger(GetSetpointCommand.class); + + private @Nullable DecimalType heatingSetpoint; + private @Nullable DecimalType coolingSetpoint; + + @Override + public byte[] getRequest() { + return MadokaMessage.createRequest(this); + } + + @Override + public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) + throws MadokaParsingException { + try { + Integer iHeatingSetpoint = (int) (mm.getValues().get(0x21).getComputedValue() / 128.); + Integer iCoolingSetpoint = (int) (mm.getValues().get(0x20).getComputedValue() / 128.); + + this.heatingSetpoint = new DecimalType(iHeatingSetpoint); + this.coolingSetpoint = new DecimalType(iCoolingSetpoint); + + logger.debug("heatingSetpoint: {}", heatingSetpoint); + logger.debug("coolingSetpoint: {}", coolingSetpoint); + + setState(State.SUCCEEDED); + executor.execute(() -> listener.receivedResponse(this)); + } catch (Exception e) { + setState(State.FAILED); + throw new MadokaParsingException(e); + } + } + + @Override + public int getCommandId() { + return 64; + } + + public @Nullable DecimalType getHeatingSetpoint() { + return heatingSetpoint; + } + + public @Nullable DecimalType getCoolingSetpoint() { + return coolingSetpoint; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetVersionCommand.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetVersionCommand.java new file mode 100644 index 0000000000000..58299fe57fab5 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetVersionCommand.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands; + +import java.util.concurrent.Executor; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException; + +/** + * This command returns the firmware version + * + * @author Benjamin Lafois - Initial contribution + * + */ +@NonNullByDefault +public class GetVersionCommand extends BRC1HCommand { + + private @Nullable String remoteControllerVersion; + private @Nullable String communicationControllerVersion; + + @Override + public byte[] getRequest() { + return MadokaMessage.createRequest(this); + } + + @Override + public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) + throws MadokaParsingException { + // In this method, we intentionally do not check for null values in mv45 and mv46. In case of null pointer + // access, it will be catched by the global exception and be reported as a Parsing Reponse exception. + byte[] mv45 = mm.getValues().get(0x45).getRawValue(); + byte[] mv46 = mm.getValues().get(0x46).getRawValue(); + + if (mv45 == null || mv45.length != 3 || mv46 == null || mv46.length != 2) { + setState(State.FAILED); + throw new MadokaParsingException("Incorrect version value"); + } + + int remoteControllerMajor = mv45[0]; + int remoteControllerMinor = mv45[1]; + int remoteControllerRevision = mv45[2]; + this.remoteControllerVersion = remoteControllerMajor + "." + remoteControllerMinor + "." + + remoteControllerRevision; + + int commControllerMajor = mv46[0]; + int commControllerMinor = mv46[1]; + this.communicationControllerVersion = commControllerMajor + "." + commControllerMinor; + + setState(State.SUCCEEDED); + executor.execute(() -> listener.receivedResponse(this)); + } + + @Override + public int getCommandId() { + return 304; + } + + public @Nullable String getRemoteControllerVersion() { + return remoteControllerVersion; + } + + public @Nullable String getCommunicationControllerVersion() { + return communicationControllerVersion; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/ResponseListener.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/ResponseListener.java new file mode 100644 index 0000000000000..0e6e592b0da9d --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/ResponseListener.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Command responses that the listener must implement + * + * @author Benjamin Lafois - Initial contribution + * + */ +@NonNullByDefault +public interface ResponseListener { + + public void receivedResponse(byte[] bytes); + + public void receivedResponse(GetVersionCommand command); + + public void receivedResponse(GetFanspeedCommand command); + + public void receivedResponse(GetOperationmodeCommand command); + + public void receivedResponse(GetPowerstateCommand command); + + public void receivedResponse(GetSetpointCommand command); + + public void receivedResponse(GetIndoorOutoorTemperatures command); + + public void receivedResponse(SetPowerstateCommand command); + + public void receivedResponse(SetSetpointCommand command); + + public void receivedResponse(SetOperationmodeCommand command); + + public void receivedResponse(SetFanspeedCommand command); + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetFanspeedCommand.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetFanspeedCommand.java new file mode 100644 index 0000000000000..994f335039a3e --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetFanspeedCommand.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands; + +import java.util.concurrent.Executor; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.util.HexUtils; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.FanSpeed; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This command sets the fanspeed for the current mode + * + * @author Benjamin Lafois - Initial contribution + * + */ +@NonNullByDefault +public class SetFanspeedCommand extends BRC1HCommand { + + private final Logger logger = LoggerFactory.getLogger(SetFanspeedCommand.class); + + private FanSpeed coolingFanSpeed; + private FanSpeed heatingFanSpeed; + + public SetFanspeedCommand(FanSpeed coolingFanSpeed, FanSpeed heatingFanSpeed) { + this.coolingFanSpeed = coolingFanSpeed; + this.heatingFanSpeed = heatingFanSpeed; + } + + @Override + public byte[] getRequest() { + MadokaValue paramCoolingFanSpeed = new MadokaValue(0x20, 1, new byte[] { (byte) coolingFanSpeed.value() }); + MadokaValue paramHeatingFanSpeed = new MadokaValue(0x21, 1, new byte[] { (byte) heatingFanSpeed.value() }); + + return MadokaMessage.createRequest(this, paramCoolingFanSpeed, paramHeatingFanSpeed); + } + + @Override + public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) { + byte[] msg = mm.getRawMessage(); + if (logger.isDebugEnabled() && msg != null) { + logger.debug("Got response for {} : {}", this.getClass().getSimpleName(), HexUtils.bytesToHex(msg)); + } + + setState(State.SUCCEEDED); + executor.execute(() -> listener.receivedResponse(this)); + } + + @Override + public int getCommandId() { + return 16464; + } + + public FanSpeed getCoolingFanSpeed() { + return coolingFanSpeed; + } + + public FanSpeed getHeatingFanSpeed() { + return heatingFanSpeed; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetOperationmodeCommand.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetOperationmodeCommand.java new file mode 100644 index 0000000000000..dfc4b578afa01 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetOperationmodeCommand.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands; + +import java.util.concurrent.Executor; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.util.HexUtils; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.OperationMode; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This command is in charge of changing the current operation mode + * + * @author Benjamin Lafois - Initial contribution + * + */ +@NonNullByDefault +public class SetOperationmodeCommand extends BRC1HCommand { + + private final Logger logger = LoggerFactory.getLogger(SetOperationmodeCommand.class); + + private OperationMode operationMode; + + public SetOperationmodeCommand(OperationMode operationMode) { + this.operationMode = operationMode; + } + + @Override + public byte[] getRequest() { + MadokaValue mv = new MadokaValue(0x20, 1, new byte[] { (byte) this.operationMode.value() }); + return MadokaMessage.createRequest(this, mv); + } + + @Override + public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) { + byte[] msg = mm.getRawMessage(); + if (logger.isDebugEnabled() && msg != null) { + logger.debug("Got response for {} : {}", this.getClass().getSimpleName(), HexUtils.bytesToHex(msg)); + } + + setState(State.SUCCEEDED); + executor.execute(() -> listener.receivedResponse(this)); + } + + @Override + public int getCommandId() { + return 16432; + } + + public OperationMode getOperationMode() { + return operationMode; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetPowerstateCommand.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetPowerstateCommand.java new file mode 100644 index 0000000000000..ba78d7da85bed --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetPowerstateCommand.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands; + +import java.util.concurrent.Executor; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.util.HexUtils; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This command is in charge of turning on or off the AC. + * + * @author Benjamin Lafois - Initial contribution + * + */ +@NonNullByDefault +public class SetPowerstateCommand extends BRC1HCommand { + + private final Logger logger = LoggerFactory.getLogger(SetPowerstateCommand.class); + + private OnOffType powerState; + + public SetPowerstateCommand(OnOffType powerState) { + this.powerState = powerState; + } + + @Override + public byte[] getRequest() { + MadokaValue mv = new MadokaValue(0x20, 1, + new byte[] { (byte) (this.powerState == OnOffType.ON ? 0x01 : 0x00) }); + + return MadokaMessage.createRequest(this, mv); + } + + @Override + public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) { + byte[] msg = mm.getRawMessage(); + if (logger.isDebugEnabled() && msg != null) { + logger.debug("Got response for {} : {}", this.getClass().getSimpleName(), HexUtils.bytesToHex(msg)); + } + + setState(State.SUCCEEDED); + executor.execute(() -> listener.receivedResponse(this)); + } + + @Override + public int getCommandId() { + return 16416; + } + + public OnOffType getPowerState() { + return powerState; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetSetpointCommand.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetSetpointCommand.java new file mode 100644 index 0000000000000..f27c3d48ed1fd --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetSetpointCommand.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands; + +import java.nio.ByteBuffer; +import java.util.concurrent.Executor; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.util.HexUtils; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * THis command is in charge of changing the AC setpoint + * + * @author Benjamin Lafois - Initial contribution + * + */ +@NonNullByDefault +public class SetSetpointCommand extends BRC1HCommand { + + private final Logger logger = LoggerFactory.getLogger(SetSetpointCommand.class); + + private DecimalType coolingSetpoint; + private DecimalType heatingSetpoint; + + public SetSetpointCommand(DecimalType coolingSetpoint, DecimalType heatingSetpoint) { + this.coolingSetpoint = coolingSetpoint; + this.heatingSetpoint = heatingSetpoint; + } + + @Override + public byte[] getRequest() { + byte[] heatingSetpointBytes = ByteBuffer.allocate(2).putShort((short) (128. * heatingSetpoint.shortValue())) + .array(); + byte[] coolingSetpointBytes = ByteBuffer.allocate(2).putShort((short) (128. * coolingSetpoint.shortValue())) + .array(); + + MadokaValue mvHeatingSetpoint = new MadokaValue(0x21, 2, heatingSetpointBytes); + + MadokaValue mvCoolingSetpoint = new MadokaValue(0x20, 2, coolingSetpointBytes); + + return MadokaMessage.createRequest(this, mvCoolingSetpoint, mvHeatingSetpoint); + } + + @Override + public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) { + byte[] msg = mm.getRawMessage(); + if (logger.isDebugEnabled() && msg != null) { + logger.debug("Got response for {} : {}", this.getClass().getSimpleName(), HexUtils.bytesToHex(msg)); + } + + setState(State.SUCCEEDED); + executor.execute(() -> listener.receivedResponse(this)); + } + + @Override + public int getCommandId() { + return 16448; + } + + public DecimalType getCoolingSetpoint() { + return coolingSetpoint; + } + + public DecimalType getHeatingSetpoint() { + return heatingSetpoint; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/resources/ESH-INF/thing/daikinmadoka.xml b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/resources/ESH-INF/thing/daikinmadoka.xml new file mode 100644 index 0000000000000..6cd7e2564530d --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/resources/ESH-INF/thing/daikinmadoka.xml @@ -0,0 +1,133 @@ + + + + + + + + + + A Daikin Madoka BRC1H Thermostat (BLE) + + + + + + + + + + + + + + + + + + + + Bluetooth address in XX:XX:XX:XX:XX:XX format + + + + + true + Refresh interval for battery and light sensor data (in seconds). This could impact battery lifetime + 60 + + + + true + The amount of time, in milliseconds, a command should take before it times out. + 1000 + + + + + + Switch + + + + + Number:Temperature + + + + + Number:Temperature + + + + + String + + + + + String + + + + + String + + + + + + + + + + + + + + + Number:Temperature + + + + + Number + + + + + String + + Readonly value. Off, Heating, Cooling, Auto + + + + String + + + + + + + + + + + + + Number + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/test/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DummyThing.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/test/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DummyThing.java new file mode 100644 index 0000000000000..6b102876510f0 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/test/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DummyThing.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.handler; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.core.Configuration; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.ThingStatusInfo; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; + +/** + * + * @author Benjamin Lafois + * + */ +@NonNullByDefault +public class DummyThing implements Thing { + + @Override + public @Nullable String getLabel() { + return null; + } + + @Override + public void setLabel(@Nullable String label) { + } + + @Override + public List<@NonNull Channel> getChannels() { + return new ArrayList(); + } + + @Override + public List<@NonNull Channel> getChannelsOfGroup(String channelGroupId) { + return new ArrayList(); + } + + @Override + public @Nullable Channel getChannel(String channelId) { + return null; + } + + @Override + public @Nullable Channel getChannel(ChannelUID channelUID) { + return null; + } + + @Override + public ThingStatus getStatus() { + return ThingStatus.ONLINE; + } + + @Override + public ThingStatusInfo getStatusInfo() { + return new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Dummy"); + } + + @Override + public void setStatusInfo(ThingStatusInfo status) { + } + + @Override + public void setHandler(@Nullable ThingHandler thingHandler) { + } + + @Override + public @Nullable ThingHandler getHandler() { + return null; + } + + @Override + public @Nullable ThingUID getBridgeUID() { + return null; + } + + @Override + public void setBridgeUID(@Nullable ThingUID bridgeUID) { + } + + @Override + public Configuration getConfiguration() { + return new Configuration(); + } + + @Override + public ThingUID getUID() { + return new ThingUID("dummy"); + } + + @Override + public ThingTypeUID getThingTypeUID() { + return new ThingTypeUID("dummy"); + } + + @Override + public Map<@NonNull String, @NonNull String> getProperties() { + return new HashMap(); + } + + @Override + public @Nullable String setProperty(String name, @Nullable String value) { + return null; + } + + @Override + public void setProperties(Map<@NonNull String, @NonNull String> properties) { + } + + @Override + public @Nullable String getLocation() { + return null; + } + + @Override + public void setLocation(@Nullable String location) { + } + + @Override + public boolean isEnabled() { + return false; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/test/java/org/openhab/binding/bluetooth/daikinmadoka/internal/MadokaMessageTest.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/test/java/org/openhab/binding/bluetooth/daikinmadoka/internal/MadokaMessageTest.java new file mode 100644 index 0000000000000..9309807fda697 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/test/java/org/openhab/binding/bluetooth/daikinmadoka/internal/MadokaMessageTest.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth.daikinmadoka.internal; + +import static org.junit.Assert.assertArrayEquals; + +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.junit.Test; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaValue; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetIndoorOutoorTemperatures; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetPowerstateCommand; + +/** + * + * @author blafois + * + */ +public class MadokaMessageTest { + + @Test + public void testMessageBuildTemperature() { + byte[] resp = MadokaMessage.createRequest(new GetIndoorOutoorTemperatures()); + assertArrayEquals(resp, new byte[] { 0x00, 0x06, 0x00, 0x01, 0x10, 0x00, 0x00 }); + } + + @Test + public void testMessageBuildSetPower() { + boolean powered = true; + MadokaValue mv = new MadokaValue(0x20, 1, new byte[] { 1 }); + byte[] resp = MadokaMessage.createRequest(new SetPowerstateCommand(OnOffType.ON), mv); + assertArrayEquals( + new byte[] { 0x00, 0x07, 0x00, 0x40, 0x20, 0x20, 0x01, (byte) (powered == true ? 0x01 : 0x00) }, resp); + } + +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 6d406d78307b1..69f530359ab66 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -58,6 +58,7 @@ org.openhab.binding.bluetooth.bluegiga org.openhab.binding.bluetooth.bluez org.openhab.binding.bluetooth.blukii + org.openhab.binding.bluetooth.daikinmadoka org.openhab.binding.bluetooth.ruuvitag org.openhab.binding.boschindego org.openhab.binding.bosesoundtouch diff --git a/features/openhab-addons/src/main/resources/footer.xml b/features/openhab-addons/src/main/resources/footer.xml index cea0ca8231d4d..801c9626963ac 100644 --- a/features/openhab-addons/src/main/resources/footer.xml +++ b/features/openhab-addons/src/main/resources/footer.xml @@ -9,6 +9,7 @@ mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.ruuvitag/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.bluez/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.bluegiga/${project.version} + mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.daikinmadoka/${project.version} openhab-runtime-base From 5872892f647c4f5f7be40445f8ac7f3f5115ccda Mon Sep 17 00:00:00 2001 From: robnielsen Date: Tue, 30 Jun 2020 22:44:48 -0500 Subject: [PATCH 25/85] [insteon] added support for motion sensor II (2844-222) (#8014) Signed-off-by: Rob Nielsen --- bundles/org.openhab.binding.insteon/README.md | 27 ++++++- .../insteon/internal/InsteonBinding.java | 1 + .../internal/InsteonBindingConstants.java | 3 + .../internal/device/CommandHandler.java | 13 ++-- .../internal/device/MessageHandler.java | 68 +++++++++++++---- .../binding/insteon/internal/driver/Port.java | 9 ++- .../handler/InsteonDeviceHandler.java | 74 +++++++++++++------ .../resources/ESH-INF/thing/thing-types.xml | 16 ++++ .../src/main/resources/device_features.xml | 10 +++ .../src/main/resources/device_types.xml | 11 +++ 10 files changed, 182 insertions(+), 50 deletions(-) diff --git a/bundles/org.openhab.binding.insteon/README.md b/bundles/org.openhab.binding.insteon/README.md index e8d1e20d2befa..a4c9ed5924704 100644 --- a/bundles/org.openhab.binding.insteon/README.md +++ b/bundles/org.openhab.binding.insteon/README.md @@ -102,6 +102,7 @@ These have been tested and should work out of the box: | 2413U | PowerLinc 2413U USB modem | 0x000045 | Bernd Pfrommer | | 2843-222 | Wireless Open/Close Sensor | 0x000049 | Josenivaldo Benito | | 2842-222 | Motion Sensor | 0x00004A | Bernd Pfrommer | +| 2844-222 | Motion Sensor II | F00.00.24 | Rob Nielsen | | 2486DWH8 | KeypadLinc Dimmer | 0x000051 | Chris Graham | | 2472D | OutletLincDimmer | 0x000068 | Chris Graham | | X10 switch | generic X10 switch | X00.00.01 | Bernd Pfrommer | @@ -118,6 +119,7 @@ In order to determine which channels a device supports, you can look at the devi | acDelay | Number | AC Delay | | backlightDuration | Number | Back Light Duration | | batteryLevel | Number | Battery Level | +| batteryPercent | Number:Dimensionless | Battery Percent | | batteryWatermarkLevel | Number | Battery Watermark Level | | beep | Switch | Beep | | bottomOutlet | Switch | Bottom Outlet | @@ -179,7 +181,9 @@ In order to determine which channels a device supports, you can look at the devi | stage1Duration | Number | Stage 1 Duration | | switch | Switch | Switch | | systemMode | Number | System Mode | +| tamperSwitch | Contact | Tamper Switch | | temperature | Number:Temperature | Temperature | +| temperatureLevel | Number | Temperature Level | | topOutlet | Switch | Top Outlet | | update | Switch | Update | | watts | Number:Power | Watts | @@ -388,12 +392,29 @@ Then create entries in the .items file like this: ``` Contact motionSensor "motion sensor [MAP(contact.map):%s]" { channel="insteon:device:home:AABBCC:contact"} - Number motionSensorBatteryLevel "motion sensor battery level [%.1f]" { channel="insteon:device:home:AABBCC:batteryLevel" } - Number motionSensorLightLevel "motion sensor light level [%.1f]" { channel="insteon:device:home:AABBCC:lightLevel" } + Number motionSensorBatteryLevel "motion sensor battery level" { channel="insteon:device:home:AABBCC:batteryLevel" } + Number motionSensorLightLevel "motion sensor light level" { channel="insteon:device:home:AABBCC:lightLevel" } ``` This will give you a contact, the battery level, and the light level. -Note that battery and light level are only updated when either there is motion, or the sensor battery runs low. +Note that battery and light level are only updated when either there is motion, light level above/below threshold, tamper switch activated, or the sensor battery runs low. + +The motion sensor II includes three additional channels: + +**Items** + +``` + Number motionSensorBatteryPercent "motion sensor battery percent" { channel="insteon:device:home:AABBCC:batteryPercent" } + Contact motionSensorTamperSwitch "motion sensor tamper switch [MAP(contact.map):%s]" { channel="insteon:device:home:AABBCC:tamperSwitch"} + Number motionSensorTemperatureLevel "motion sensor temperature level" { channel="insteon:device:home:AABBCC:temperatureLevel" } +``` + +The temperature can be calculated in Fahrenheit using the following formulas: + +* If the device is battery powered: `temperature = 0.73 * motionSensorTemperatureLevel - 20.53` +* If the device is USB powered: `temperature = 0.72 * motionSensorTemperatureLevel - 24.61` + +Since the motion sensor II might not be calibrated correctly, the values `20.53` and `24.61` can be adjusted as necessary to produce the correct temperature. ### Hidden Door Sensors diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBinding.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBinding.java index 0affda35cc761..fae0a67766091 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBinding.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBinding.java @@ -248,6 +248,7 @@ public InsteonDevice makeNewDevice(InsteonAddress addr, String productKey) { DeviceType dt = DeviceTypeLoader.instance().getDeviceType(productKey); InsteonDevice dev = InsteonDevice.makeDevice(dt); dev.setAddress(addr); + dev.setProductKey(productKey); dev.setDriver(driver); if (!dev.hasValidPollingInterval()) { dev.setPollInterval(devicePollIntervalMilliseconds); diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java index 9040d5afa48a6..e23fd98520692 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java @@ -33,6 +33,7 @@ public class InsteonBindingConstants { public static final String AC_DELAY = "acDelay"; public static final String BACKLIGHT_DURATION = "backlightDuration"; public static final String BATTERY_LEVEL = "batteryLevel"; + public static final String BATTERY_PERCENT = "batteryPercent"; public static final String BATTERY_WATERMARK_LEVEL = "batteryWatermarkLevel"; public static final String BEEP = "beep"; public static final String BOTTOM_OUTLET = "bottomOutlet"; @@ -102,7 +103,9 @@ public class InsteonBindingConstants { public static final String STAGE1_DURATION = "stage1Duration"; public static final String SWITCH = "switch"; public static final String SYSTEM_MODE = "systemMode"; + public static final String TAMPER_SWITCH = "tamperSwitch"; public static final String TEMPERATURE = "temperature"; + public static final String TEMPERATURE_LEVEL = "temperatureLevel"; public static final String TOP_OUTLET = "topOutlet"; public static final String UPDATE = "update"; public static final String WATTS = "watts"; diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/CommandHandler.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/CommandHandler.java index 4a84247338f66..ad1d58297d94d 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/CommandHandler.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/CommandHandler.java @@ -28,6 +28,7 @@ import org.eclipse.smarthome.core.types.Command; import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration; import org.openhab.binding.insteon.internal.device.DeviceFeatureListener.StateChangeType; +import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler; import org.openhab.binding.insteon.internal.message.FieldException; import org.openhab.binding.insteon.internal.message.InvalidMessageTypeException; import org.openhab.binding.insteon.internal.message.Msg; @@ -688,23 +689,25 @@ public static class PowerMeterCommandHandler extends CommandHandler { @Override public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) { - String cmdParam = conf.getParameters().get("cmd"); + String cmdParam = conf.getParameters().get(InsteonDeviceHandler.CMD); if (cmdParam == null) { logger.warn("{} ignoring cmd {} because no cmd= is configured!", nm(), cmd); return; } try { if (cmd == OnOffType.ON) { - if (cmdParam.equals("reset")) { + if (cmdParam.equals(InsteonDeviceHandler.CMD_RESET)) { Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x80, (byte) 0x00); dev.enqueueMessage(m, feature); logger.debug("{}: sent reset msg to power meter {}", nm(), dev.getAddress()); - feature.publish(OnOffType.OFF, StateChangeType.ALWAYS, "cmd", "reset"); - } else if (cmdParam.equals("update")) { + feature.publish(OnOffType.OFF, StateChangeType.ALWAYS, InsteonDeviceHandler.CMD, + InsteonDeviceHandler.CMD_RESET); + } else if (cmdParam.equals(InsteonDeviceHandler.CMD_UPDATE)) { Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x82, (byte) 0x00); dev.enqueueMessage(m, feature); logger.debug("{}: sent update msg to power meter {}", nm(), dev.getAddress()); - feature.publish(OnOffType.OFF, StateChangeType.ALWAYS, "cmd", "update"); + feature.publish(OnOffType.OFF, StateChangeType.ALWAYS, InsteonDeviceHandler.CMD, + InsteonDeviceHandler.CMD_UPDATE); } else { logger.warn("{}: ignoring unknown cmd {} for power meter {}", nm(), cmdParam, dev.getAddress()); } diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/MessageHandler.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/MessageHandler.java index bfdf7e699d06b..0b6028bdff07d 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/MessageHandler.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/MessageHandler.java @@ -32,6 +32,7 @@ import org.eclipse.smarthome.core.types.State; import org.openhab.binding.insteon.internal.device.DeviceFeatureListener.StateChangeType; import org.openhab.binding.insteon.internal.device.GroupMessageStateMachine.GroupMessage; +import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler; import org.openhab.binding.insteon.internal.message.FieldException; import org.openhab.binding.insteon.internal.message.InvalidMessageTypeException; import org.openhab.binding.insteon.internal.message.Msg; @@ -715,15 +716,46 @@ public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) { } try { int cmd2 = msg.getByte("command2") & 0xff; + int batteryLevel; + int lightLevel; + int temperatureLevel; switch (cmd2) { case 0x00: // this is a product data response message - int batteryLevel = msg.getByte("userData12") & 0xff; - int lightLevel = msg.getByte("userData11") & 0xff; + batteryLevel = msg.getByte("userData12") & 0xff; + lightLevel = msg.getByte("userData11") & 0xff; logger.debug("{}: {} got light level: {}, battery level: {}", nm(), dev.getAddress(), lightLevel, batteryLevel); - feature.publish(new DecimalType(lightLevel), StateChangeType.CHANGED, "field", "light_level"); - feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED, "field", - "battery_level"); + feature.publish(new DecimalType(lightLevel), StateChangeType.CHANGED, + InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_LIGHT_LEVEL); + feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED, + InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_LEVEL); + break; + case 0x03: // this is the 2844-222 data response message + batteryLevel = msg.getByte("userData6") & 0xff; + lightLevel = msg.getByte("userData7") & 0xff; + temperatureLevel = msg.getByte("userData8") & 0xff; + logger.debug("{}: {} got light level: {}, battery level: {}, temperature level: {}", nm(), + dev.getAddress(), lightLevel, batteryLevel, temperatureLevel); + feature.publish(new DecimalType(lightLevel), StateChangeType.CHANGED, + InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_LIGHT_LEVEL); + feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED, + InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_LEVEL); + feature.publish(new DecimalType(temperatureLevel), StateChangeType.CHANGED, + InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_TEMPERATURE_LEVEL); + + // per 2844-222 dev doc: working battery level range is 0xd2 - 0x70 + int batteryPercentage; + if (batteryLevel >= 0xd2) { + batteryPercentage = 100; + } else if (batteryLevel <= 0x70) { + batteryPercentage = 0; + } else { + batteryPercentage = (batteryLevel - 0x70) * 100 / (0xd2 - 0x70); + } + logger.debug("{}: {} battery percentage: {}", nm(), dev.getAddress(), batteryPercentage); + feature.publish(new QuantityType<>(batteryPercentage, SmartHomeUnits.PERCENT), + StateChangeType.CHANGED, InsteonDeviceHandler.FIELD, + InsteonDeviceHandler.FIELD_BATTERY_PERCENTAGE); break; default: logger.warn("unknown cmd2 = {} in info reply message {}", cmd2, msg); @@ -756,10 +788,10 @@ public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) { int batteryWatermark = msg.getByte("userData7") & 0xff; logger.debug("{}: {} got light level: {}, battery level: {}", nm(), dev.getAddress(), batteryWatermark, batteryLevel); - feature.publish(new DecimalType(batteryWatermark), StateChangeType.CHANGED, "field", - "battery_watermark_level"); - feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED, "field", - "battery_level"); + feature.publish(new DecimalType(batteryWatermark), StateChangeType.CHANGED, + InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_WATERMARK_LEVEL); + feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED, + InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_LEVEL); break; default: logger.warn("unknown cmd2 = {} in info reply message {}", cmd2, msg); @@ -801,9 +833,9 @@ public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) { logger.debug("{}:{} watts: {} kwh: {} ", nm(), f.getDevice().getAddress(), watts, kwh); feature.publish(new QuantityType<>(kwh, SmartHomeUnits.KILOWATT_HOUR), StateChangeType.CHANGED, - "field", "kwh"); - feature.publish(new QuantityType<>(watts, SmartHomeUnits.WATT), StateChangeType.CHANGED, "field", - "watts"); + InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_KWH); + feature.publish(new QuantityType<>(watts, SmartHomeUnits.WATT), StateChangeType.CHANGED, + InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_WATTS); } catch (FieldException e) { logger.warn("error parsing {}: ", msg, e); } @@ -941,7 +973,11 @@ public static class ClosedSleepingContactHandler extends MessageHandler { @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) { feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS); - sendExtendedQuery(f, (byte) 0x2e, (byte) 00); + if (f.getDevice().hasProductKey(InsteonDeviceHandler.MOTION_SENSOR_II_PRODUCT_KEY)) { + sendExtendedQuery(f, (byte) 0x2e, (byte) 03); + } else { + sendExtendedQuery(f, (byte) 0x2e, (byte) 00); + } } } @@ -954,7 +990,11 @@ public static class OpenedSleepingContactHandler extends MessageHandler { @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) { feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS); - sendExtendedQuery(f, (byte) 0x2e, (byte) 00); + if (f.getDevice().hasProductKey(InsteonDeviceHandler.MOTION_SENSOR_II_PRODUCT_KEY)) { + sendExtendedQuery(f, (byte) 0x2e, (byte) 03); + } else { + sendExtendedQuery(f, (byte) 0x2e, (byte) 00); + } } } diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/driver/Port.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/driver/Port.java index e10313f6f7d57..4c7484189a313 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/driver/Port.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/driver/Port.java @@ -28,6 +28,7 @@ import org.openhab.binding.insteon.internal.device.InsteonAddress; import org.openhab.binding.insteon.internal.device.InsteonDevice; import org.openhab.binding.insteon.internal.device.ModemDBBuilder; +import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler; import org.openhab.binding.insteon.internal.message.FieldException; import org.openhab.binding.insteon.internal.message.InvalidMessageTypeException; import org.openhab.binding.insteon.internal.message.Msg; @@ -509,14 +510,14 @@ public void msg(Msg msg) { if (msg.getByte("Cmd") == 0x60) { // add the modem to the device list InsteonAddress a = new InsteonAddress(msg.getAddress("IMAddress")); - String prodKey = "0x000045"; - DeviceType dt = DeviceTypeLoader.instance().getDeviceType(prodKey); + DeviceType dt = DeviceTypeLoader.instance().getDeviceType(InsteonDeviceHandler.PLM_PRODUCT_KEY); if (dt == null) { - logger.warn("unknown modem product key: {} for modem: {}.", prodKey, a); + logger.warn("unknown modem product key: {} for modem: {}.", + InsteonDeviceHandler.PLM_PRODUCT_KEY, a); } else { device = InsteonDevice.makeDevice(dt); device.setAddress(a); - device.setProductKey(prodKey); + device.setProductKey(InsteonDeviceHandler.PLM_PRODUCT_KEY); device.setDriver(driver); device.setIsModem(true); logger.debug("found modem {} in device_types: {}", a, device.toString()); diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/handler/InsteonDeviceHandler.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/handler/InsteonDeviceHandler.java index 4acb2cce0de91..c4f19cb06604a 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/handler/InsteonDeviceHandler.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/handler/InsteonDeviceHandler.java @@ -55,11 +55,12 @@ public class InsteonDeviceHandler extends BaseThingHandler { private static final Set ALL_CHANNEL_IDS = Collections.unmodifiableSet(Stream.of( InsteonBindingConstants.AC_DELAY, InsteonBindingConstants.BACKLIGHT_DURATION, - InsteonBindingConstants.BATTERY_LEVEL, InsteonBindingConstants.BATTERY_WATERMARK_LEVEL, - InsteonBindingConstants.BEEP, InsteonBindingConstants.BOTTOM_OUTLET, InsteonBindingConstants.BUTTON_A, - InsteonBindingConstants.BUTTON_B, InsteonBindingConstants.BUTTON_C, InsteonBindingConstants.BUTTON_D, - InsteonBindingConstants.BUTTON_E, InsteonBindingConstants.BUTTON_F, InsteonBindingConstants.BUTTON_G, - InsteonBindingConstants.BUTTON_H, InsteonBindingConstants.BROADCAST_ON_OFF, InsteonBindingConstants.CONTACT, + InsteonBindingConstants.BATTERY_LEVEL, InsteonBindingConstants.BATTERY_PERCENT, + InsteonBindingConstants.BATTERY_WATERMARK_LEVEL, InsteonBindingConstants.BEEP, + InsteonBindingConstants.BOTTOM_OUTLET, InsteonBindingConstants.BUTTON_A, InsteonBindingConstants.BUTTON_B, + InsteonBindingConstants.BUTTON_C, InsteonBindingConstants.BUTTON_D, InsteonBindingConstants.BUTTON_E, + InsteonBindingConstants.BUTTON_F, InsteonBindingConstants.BUTTON_G, InsteonBindingConstants.BUTTON_H, + InsteonBindingConstants.BROADCAST_ON_OFF, InsteonBindingConstants.CONTACT, InsteonBindingConstants.COOL_SET_POINT, InsteonBindingConstants.DIMMER, InsteonBindingConstants.FAN, InsteonBindingConstants.FAN_MODE, InsteonBindingConstants.FAST_ON_OFF, InsteonBindingConstants.FAST_ON_OFF_BUTTON_A, InsteonBindingConstants.FAST_ON_OFF_BUTTON_B, @@ -86,28 +87,32 @@ public class InsteonDeviceHandler extends BaseThingHandler { InsteonBindingConstants.MANUAL_CHANGE_BUTTON_H, InsteonBindingConstants.NOTIFICATION, InsteonBindingConstants.ON_LEVEL, InsteonBindingConstants.RAMP_DIMMER, InsteonBindingConstants.RAMP_RATE, InsteonBindingConstants.RESET, InsteonBindingConstants.STAGE1_DURATION, InsteonBindingConstants.SWITCH, - InsteonBindingConstants.SYSTEM_MODE, InsteonBindingConstants.TEMPERATURE, + InsteonBindingConstants.SYSTEM_MODE, InsteonBindingConstants.TAMPER_SWITCH, + InsteonBindingConstants.TEMPERATURE, InsteonBindingConstants.TEMPERATURE_LEVEL, InsteonBindingConstants.TOP_OUTLET, InsteonBindingConstants.UPDATE, InsteonBindingConstants.WATTS) .collect(Collectors.toSet())); - private static final String BROADCAST_ON_OFF = "broadcastonoff"; - private static final String CMD = "cmd"; - private static final String CMD_RESET = "reset"; - private static final String CMD_UPDATE = "update"; - private static final String DATA = "data"; - private static final String FIELD = "field"; - private static final String FIELD_BATTERY_LEVEL = "battery_level"; - private static final String FIELD_BATTERY_WATERMARK_LEVEL = "battery_watermark_level"; - private static final String FIELD_KWH = "kwh"; - private static final String FIELD_LIGHT_LEVEL = "light_level"; - private static final String FIELD_WATTS = "watts"; - private static final String GROUP = "group"; - private static final String METER = "meter"; - - private static final String HIDDEN_DOOR_SENSOR_PRODUCT_KEY = "F00.00.03"; - private static final String MOTION_SENSOR_PRODUCT_KEY = "0x00004A"; - private static final String PLM_PRODUCT_KEY = "0x000045"; - private static final String POWER_METER_PRODUCT_KEY = "F00.00.17"; + public static final String BROADCAST_ON_OFF = "broadcastonoff"; + public static final String CMD = "cmd"; + public static final String CMD_RESET = "reset"; + public static final String CMD_UPDATE = "update"; + public static final String DATA = "data"; + public static final String FIELD = "field"; + public static final String FIELD_BATTERY_LEVEL = "battery_level"; + public static final String FIELD_BATTERY_PERCENTAGE = "battery_percentage"; + public static final String FIELD_BATTERY_WATERMARK_LEVEL = "battery_watermark_level"; + public static final String FIELD_KWH = "kwh"; + public static final String FIELD_LIGHT_LEVEL = "light_level"; + public static final String FIELD_TEMPERATURE_LEVEL = "temperature_level"; + public static final String FIELD_WATTS = "watts"; + public static final String GROUP = "group"; + public static final String METER = "meter"; + + public static final String HIDDEN_DOOR_SENSOR_PRODUCT_KEY = "F00.00.03"; + public static final String MOTION_SENSOR_II_PRODUCT_KEY = "F00.00.24"; + public static final String MOTION_SENSOR_PRODUCT_KEY = "0x00004A"; + public static final String PLM_PRODUCT_KEY = "0x000045"; + public static final String POWER_METER_PRODUCT_KEY = "F00.00.17"; private final Logger logger = LoggerFactory.getLogger(InsteonDeviceHandler.class); @@ -175,6 +180,13 @@ public void initialize() { || feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)) { feature = DATA; } + } else if (productKey.equals(MOTION_SENSOR_II_PRODUCT_KEY)) { + if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL) + || feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_PERCENT) + || feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL) + || feature.equalsIgnoreCase(InsteonBindingConstants.TEMPERATURE_LEVEL)) { + feature = DATA; + } } else if (productKey.equals(PLM_PRODUCT_KEY)) { String parts[] = feature.split("#"); if (parts.length == 2 && parts[0].equalsIgnoreCase(InsteonBindingConstants.BROADCAST_ON_OFF) @@ -312,6 +324,20 @@ public void channelLinked(ChannelUID channelUID) { params.put(FIELD, FIELD_LIGHT_LEVEL); feature = DATA; } + } else if (productKey.equals(MOTION_SENSOR_II_PRODUCT_KEY)) { + if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)) { + params.put(FIELD, FIELD_BATTERY_LEVEL); + feature = DATA; + } else if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_PERCENT)) { + params.put(FIELD, FIELD_BATTERY_PERCENTAGE); + feature = DATA; + } else if (feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)) { + params.put(FIELD, FIELD_LIGHT_LEVEL); + feature = DATA; + } else if (feature.equalsIgnoreCase(InsteonBindingConstants.TEMPERATURE_LEVEL)) { + params.put(FIELD, FIELD_TEMPERATURE_LEVEL); + feature = DATA; + } } else if (productKey.equals(PLM_PRODUCT_KEY)) { String parts[] = feature.split("#"); if (parts.length == 2 && parts[0].equalsIgnoreCase(InsteonBindingConstants.BROADCAST_ON_OFF) diff --git a/bundles/org.openhab.binding.insteon/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.insteon/src/main/resources/ESH-INF/thing/thing-types.xml index efe3885aea8b3..b93f72c14ed47 100644 --- a/bundles/org.openhab.binding.insteon/src/main/resources/ESH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.insteon/src/main/resources/ESH-INF/thing/thing-types.xml @@ -81,6 +81,7 @@ + @@ -114,6 +115,11 @@ + + Number:Dimensionless + + + Number @@ -459,11 +465,21 @@ + + Contact + + + Number:Temperature + + Number + + + Switch diff --git a/bundles/org.openhab.binding.insteon/src/main/resources/device_features.xml b/bundles/org.openhab.binding.insteon/src/main/resources/device_features.xml index 6dbfd7fb1bf30..5e42b34c691db 100644 --- a/bundles/org.openhab.binding.insteon/src/main/resources/device_features.xml +++ b/bundles/org.openhab.binding.insteon/src/main/resources/device_features.xml @@ -423,6 +423,16 @@ NoOpCommandHandler NoPollHandler + + DefaultDispatcher + NoOpMsgHandler + OpenedSleepingContactHandler + ClosedSleepingContactHandler + NoOpMsgHandler + NoOpMsgHandler + NoOpCommandHandler + NoPollHandler + SimpleDispatcher NoOpMsgHandler diff --git a/bundles/org.openhab.binding.insteon/src/main/resources/device_types.xml b/bundles/org.openhab.binding.insteon/src/main/resources/device_types.xml index 14d567c881fd1..7775b88f8facf 100644 --- a/bundles/org.openhab.binding.insteon/src/main/resources/device_types.xml +++ b/bundles/org.openhab.binding.insteon/src/main/resources/device_types.xml @@ -584,4 +584,15 @@ GenericLastTime
+ + + 2844-222 + Motion Sensor II + WirelessMotionSensorContact + WirelessMotionSensorLightLevelAboveThreshold + WirelessMotionSensorLowBattery + MotionSensorData + WirelessMotionSensor2TamperSwitch + GenericLastTime + From a625f412b357b8af8b75c4f7810e99b6568262f1 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Wed, 1 Jul 2020 21:51:34 +0200 Subject: [PATCH 26/85] [lcn] Add ability to invert Rollershutter Up/Down (#8051) Signed-off-by: Fabian Wolter --- bundles/org.openhab.binding.lcn/README.md | 2 ++ bundles/org.openhab.binding.lcn/pom.xml | 4 +++- .../lcn/internal/LcnModuleHandler.java | 7 ++++++- .../AbstractLcnModuleSubHandler.java | 3 ++- .../subhandler/ILcnModuleSubHandler.java | 4 +++- ...cnModuleRollershutterOutputSubHandler.java | 5 +++-- ...LcnModuleRollershutterRelaySubHandler.java | 7 ++++--- .../resources/ESH-INF/thing/thing-types.xml | 8 ++++++++ ...duleRollershutterOutputSubHandlerTest.java | 16 +++++++++++++-- ...oduleRollershutterRelaySubHandlerTest.java | 20 +++++++++++++++---- 10 files changed, 61 insertions(+), 15 deletions(-) diff --git a/bundles/org.openhab.binding.lcn/README.md b/bundles/org.openhab.binding.lcn/README.md index 6226424c7e662..1d99f8e427c0e 100644 --- a/bundles/org.openhab.binding.lcn/README.md +++ b/bundles/org.openhab.binding.lcn/README.md @@ -278,6 +278,8 @@ If a special command is needed, the [Hit Key](#hit-key) action (German: "Sende T S0 counter Channels need to be the pulses per kWh configured. If the value is left blank, a default value of 1000 pulses/kWh is set. +The Rollershutter Channels provide the boolean parameter `invertUpDown`, which can be set to 'true' if the Up/Down wires are interchanged. + ### Transponder LCN transponder readers can be integrated in openHAB e.g. for access control. diff --git a/bundles/org.openhab.binding.lcn/pom.xml b/bundles/org.openhab.binding.lcn/pom.xml index 9819d1149049b..ae8894d181b55 100644 --- a/bundles/org.openhab.binding.lcn/pom.xml +++ b/bundles/org.openhab.binding.lcn/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleHandler.java index 32ea98c7e35b3..6fd07bb345161 100644 --- a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleHandler.java +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleHandler.java @@ -176,7 +176,12 @@ public void handleCommand(ChannelUID channelUid, Command command) { DecimalType nativeValue = getConverter(channelUid).onCommandFromItem(quantityType); subHandler.handleCommandDecimal(nativeValue, channelGroup, number.get()); } else if (command instanceof UpDownType) { - subHandler.handleCommandUpDown((UpDownType) command, channelGroup, number.get()); + Channel channel = thing.getChannel(channelUid); + if (channel != null) { + Object invertConfig = channel.getConfiguration().get("invertUpDown"); + boolean invertUpDown = invertConfig instanceof Boolean && (boolean) invertConfig; + subHandler.handleCommandUpDown((UpDownType) command, channelGroup, number.get(), invertUpDown); + } } else if (command instanceof StopMoveType) { subHandler.handleCommandStopMove((StopMoveType) command, channelGroup, number.get()); } else { diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleSubHandler.java index 3988283d50305..869b147394065 100644 --- a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleSubHandler.java +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleSubHandler.java @@ -93,7 +93,8 @@ public void handleCommandString(StringType command, int number) throws LcnExcept } @Override - public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number) throws LcnException { + public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number, boolean invertUpDown) + throws LcnException { unsupportedCommand(command); } diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/ILcnModuleSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/ILcnModuleSubHandler.java index aff33cc1f0ebd..c963796fe8263 100644 --- a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/ILcnModuleSubHandler.java +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/ILcnModuleSubHandler.java @@ -130,9 +130,11 @@ void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, Str * @param command the command to handle * @param channelGroup the addressed Channel group * @param number the Channel's number within the Channel group + * @param invertUpDown true, if Up/Down is inverted * @throws LcnException when the command could not processed */ - void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number) throws LcnException; + void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number, boolean invertUpDown) + throws LcnException; /** * Handles a Command from openHAB. diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandler.java index 71b7521ebcd93..ae3960d755316 100644 --- a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandler.java +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandler.java @@ -44,10 +44,11 @@ public void handleRefresh(LcnChannelGroup channelGroup, int number) { } @Override - public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number) throws LcnException { + public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number, boolean invertUpDown) + throws LcnException { // When configured as shutter in LCN-PRO, an output gets switched off, when the other is // switched on and vice versa. - if (command == UpDownType.UP) { + if (command == UpDownType.UP ^ invertUpDown) { // first output: 100% handler.sendPck(PckGenerator.dimOutput(0, 100, LcnDefs.ROLLER_SHUTTER_RAMP_MS)); } else { diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandler.java index ef46add8a5f01..ce701f8b4fde0 100644 --- a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandler.java +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandler.java @@ -30,7 +30,7 @@ /** * Handles Commands and State changes of roller shutters connected to relays of an LCN module. - * + * * @author Fabian Wolter - Initial contribution */ @NonNullByDefault @@ -45,10 +45,11 @@ public void handleRefresh(LcnChannelGroup channelGroup, int number) { } @Override - public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number) throws LcnException { + public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number, boolean invertUpDown) + throws LcnException { RelayStateModifier[] relayStateModifiers = createRelayStateModifierArray(); // direction relay - relayStateModifiers[number * 2 + 1] = command == UpDownType.DOWN ? LcnDefs.RelayStateModifier.ON + relayStateModifiers[number * 2 + 1] = command == UpDownType.DOWN ^ invertUpDown ? LcnDefs.RelayStateModifier.ON : LcnDefs.RelayStateModifier.OFF; // power relay relayStateModifiers[number * 2] = LcnDefs.RelayStateModifier.ON; diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/thing/thing-types.xml index 7921cd5534715..d35174ae235cd 100644 --- a/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/thing/thing-types.xml @@ -151,6 +151,14 @@ Rollershutter veto + + + + According LCN spec., the Up wire is connected to the "normally open" contact/Output 1. Use this + parameter to invert that logic. + false + + diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandlerTest.java index 3809f0a099788..b0745ea058ffb 100644 --- a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandlerTest.java +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandlerTest.java @@ -41,16 +41,28 @@ public void setUp() { @Test public void testUp() throws LcnException { - l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0); + l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0, false); verify(handler).sendPck("A1DI100008"); } + @Test + public void testUpInverted() throws LcnException { + l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0, true); + verify(handler).sendPck("A2DI100008"); + } + @Test public void testDown() throws LcnException { - l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0); + l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0, false); verify(handler).sendPck("A2DI100008"); } + @Test + public void testDownInverted() throws LcnException { + l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0, true); + verify(handler).sendPck("A1DI100008"); + } + @Test public void testStop() throws LcnException { l.handleCommandStopMove(StopMoveType.STOP, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0); diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandlerTest.java index 99816dc13d055..806aeb69ffb5a 100644 --- a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandlerTest.java +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandlerTest.java @@ -41,25 +41,37 @@ public void setUp() { @Test public void testUp1() throws LcnException { - l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTERRELAY, 0); + l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTERRELAY, 0, false); verify(handler).sendPck("R810------"); } + @Test + public void testUpInverted() throws LcnException { + l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTERRELAY, 0, true); + verify(handler).sendPck("R811------"); + } + @Test public void testUp4() throws LcnException { - l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTERRELAY, 3); + l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTERRELAY, 3, false); verify(handler).sendPck("R8------10"); } @Test public void testDown1() throws LcnException { - l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTERRELAY, 0); + l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTERRELAY, 0, false); verify(handler).sendPck("R811------"); } + @Test + public void testDownInverted() throws LcnException { + l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTERRELAY, 0, true); + verify(handler).sendPck("R810------"); + } + @Test public void testDown4() throws LcnException { - l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTERRELAY, 3); + l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTERRELAY, 3, false); verify(handler).sendPck("R8------11"); } From 9f535cf64abcfce72bc755d329e581c743cd294e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Berland?= Date: Thu, 2 Jul 2020 10:44:54 +0200 Subject: [PATCH 27/85] [opensprinkler] Fix channel name for OpenSprinkler currentDraw in README (#8053) Merged without sign-off under small document changes exemption --- bundles/org.openhab.binding.opensprinkler/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.opensprinkler/README.md b/bundles/org.openhab.binding.opensprinkler/README.md index e42ee82f8f634..25e042b079a71 100644 --- a/bundles/org.openhab.binding.opensprinkler/README.md +++ b/bundles/org.openhab.binding.opensprinkler/README.md @@ -96,8 +96,8 @@ Switch Station04 (stations) { channel="opensprinkler:station:http:04:stationStat Switch Station05 (stations) { channel="opensprinkler:station:http:05:stationState" } Switch Station06 (stations) { channel="opensprinkler:station:http:06:stationState" } -Switch RainSensor { channel="opensprinkler:station:http:device:rainsensor" } -Number:ElectricCurrent CurrentDraw {channel="opensprinkler:station:http:device:currentDraw"} +Switch RainSensor { channel="opensprinkler:device:http:device:rainsensor" } +Number:ElectricCurrent CurrentDraw {channel="opensprinkler:device:http:device:currentDraw"} ``` demo.sitemap: From 46dc55ce92f22f99b12fe1b5a269eef68cd590b5 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 2 Jul 2020 07:00:02 -0700 Subject: [PATCH 28/85] [miio] minor docu & log update (#8046) Signed-off-by: Marcel Verpaalen --- bundles/org.openhab.binding.miio/README.base.md | 4 ++++ bundles/org.openhab.binding.miio/README.md | 4 ++++ .../openhab/binding/miio/internal/cloud/CloudConnector.java | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.miio/README.base.md b/bundles/org.openhab.binding.miio/README.base.md index 221ffeee99a7b..e66dc01061016 100644 --- a/bundles/org.openhab.binding.miio/README.base.md +++ b/bundles/org.openhab.binding.miio/README.base.md @@ -71,6 +71,10 @@ However, for devices that are unsupported, you may override the value and try to `Thing miio:basic:light "My Light" [ host="192.168.x.x", token="put here your token", deviceId="0326xxxx" ]` +or in case of unknown models include the model information e.g.: + +`Thing miio:vacuum:s50 "vacuum" @ "livingroom" [ host="192.168.15.20", token="xxxxxxx", deviceId=“0470DDAA”, model="roborock.vacuum.s5" ]` + ## Mi IO Devices !!!devices diff --git a/bundles/org.openhab.binding.miio/README.md b/bundles/org.openhab.binding.miio/README.md index 01952370a0cb1..8d9f0e1fb6152 100644 --- a/bundles/org.openhab.binding.miio/README.md +++ b/bundles/org.openhab.binding.miio/README.md @@ -71,6 +71,10 @@ However, for devices that are unsupported, you may override the value and try to `Thing miio:basic:light "My Light" [ host="192.168.x.x", token="put here your token", deviceId="0326xxxx" ]` +or in case of unknown models include the model information e.g.: + +`Thing miio:vacuum:s50 "vacuum" @ "livingroom" [ host="192.168.15.20", token="xxxxxxx", deviceId=“0470DDAA”, model="roborock.vacuum.s5" ]` + ## Mi IO Devices | Device | ThingType | Device Model | Supported | Remark | diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/cloud/CloudConnector.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/cloud/CloudConnector.java index 3d89cf9020666..3f58a2a7f3903 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/cloud/CloudConnector.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/cloud/CloudConnector.java @@ -116,7 +116,7 @@ public boolean isConnected() { } public @Nullable RawType getMap(String mapId, String country) throws MiCloudException { - logger.info("Getting vacuum map {} from Xiaomi cloud server: {}", mapId, country); + logger.debug("Getting vacuum map {} from Xiaomi cloud server: '{}'", mapId, country); String mapCountry; String mapUrl = ""; final @Nullable MiCloudConnector cl = this.cloudConnector; From 3f314cfb845813c3c5851e3a30728e4341af08b1 Mon Sep 17 00:00:00 2001 From: Benjamin Lafois Date: Thu, 2 Jul 2020 21:48:02 +0200 Subject: [PATCH 29/85] [Deconz] add support for "last_seen" channel (#7992) Signed-off-by: Benjamin Lafois --- bundles/org.openhab.binding.deconz/.classpath | 6 +++++ bundles/org.openhab.binding.deconz/README.md | 1 + .../deconz/internal/BindingConstants.java | 1 + .../internal/dto/DeconzBaseMessage.java | 4 ++++ .../handler/DeconzBaseThingHandler.java | 3 ++- .../handler/SensorBaseThingHandler.java | 18 ++++++++++++-- .../ESH-INF/thing/sensor-thing-types.xml | 24 +++++++++++++++++++ 7 files changed, 54 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.deconz/.classpath b/bundles/org.openhab.binding.deconz/.classpath index a5d95095ccaaf..1a0c5608f3440 100644 --- a/bundles/org.openhab.binding.deconz/.classpath +++ b/bundles/org.openhab.binding.deconz/.classpath @@ -28,5 +28,11 @@ + + + + + + diff --git a/bundles/org.openhab.binding.deconz/README.md b/bundles/org.openhab.binding.deconz/README.md index 7546554cc5c88..f32f64d50a86e 100644 --- a/bundles/org.openhab.binding.deconz/README.md +++ b/bundles/org.openhab.binding.deconz/README.md @@ -110,6 +110,7 @@ The sensor devices support some of the following channels: |-----------------|--------------------------|:-----------:|-------------------------------------------------------------------------------------------|----------------------------------------------| | presence | Switch | R | Status of presence: `ON` = presence; `OFF` = no-presence | presencesensor | | last_updated | DateTime | R | Timestamp when the sensor was last updated | all, except daylightsensor | +| last_seen | DateTime | R | Timestamp when the sensor was last seen | all, except daylightsensor | | power | Number:Power | R | Current power usage in Watts | powersensor, sometimes for consumptionsensor | | consumption | Number:Energy | R | Current power usage in Watts/Hour | consumptionsensor | | voltage | Number:ElectricPotential | R | Current voltage in V | some powersensors | diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java index 85df47e81470d..009d638051bfd 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java @@ -64,6 +64,7 @@ public class BindingConstants { // List of all Channel ids public static final String CHANNEL_PRESENCE = "presence"; public static final String CHANNEL_LAST_UPDATED = "last_updated"; + public static final String CHANNEL_LAST_SEEN = "last_seen"; public static final String CHANNEL_POWER = "power"; public static final String CHANNEL_CONSUMPTION = "consumption"; public static final String CHANNEL_VOLTAGE = "voltage"; diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/DeconzBaseMessage.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/DeconzBaseMessage.java index a5a96b8bfc8d8..c7a70c56f2382 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/DeconzBaseMessage.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/DeconzBaseMessage.java @@ -13,6 +13,7 @@ package org.openhab.binding.deconz.internal.dto; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; /** * The REST interface and websocket connection are using the same fields. @@ -37,6 +38,9 @@ public class DeconzBaseMessage { /** the API endpoint **/ public String ep = ""; + /** device last seen */ + public @Nullable String lastseen; + // websocket and rest api public String uniqueid = ""; // "00:0b:57:ff:fe:94:6b:dd-01-1000" } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java index 3c212d13ab147..9b25a7072ec73 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java @@ -55,7 +55,8 @@ public abstract class DeconzBaseThingHandler extend protected ThingConfig config = new ThingConfig(); protected DeconzBridgeConfig bridgeConfig = new DeconzBridgeConfig(); protected final Gson gson; - private @Nullable ScheduledFuture scheduledFuture; + @Nullable + protected ScheduledFuture scheduledFuture; protected @Nullable WebSocketConnection connection; protected @Nullable AsyncHttpClient http; diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java index 3a86375575874..d139ddab29804 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java @@ -21,6 +21,7 @@ import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import javax.measure.Unit; @@ -172,6 +173,19 @@ protected void processStateResponse(@Nullable SensorMessage stateResponse) { updateChannels(sensorConfig); updateChannels(sensorState, true); + // "Last seen" is the last "ping" from the device, whereas "last update" is the last status changed. + // For example, for a fire sensor, the device pings regularly, without necessarily updating channels. + // So to monitor a sensor is still alive, the "last seen" is necessary. + if (stateResponse.lastseen != null) { + updateState(CHANNEL_LAST_SEEN, + new DateTimeType(ZonedDateTime.ofInstant( + LocalDateTime.parse(stateResponse.lastseen, DateTimeFormatter.ISO_LOCAL_DATE_TIME), + ZoneOffset.UTC, ZoneId.systemDefault()))); + // Because "last seen" is never updated by the WebSocket API - if this is supported, then we have to + // manually poll it time to time (every 5 minutes by default) + super.scheduledFuture = scheduler.schedule((Runnable) this::requestState, 5, TimeUnit.MINUTES); + } + updateStatus(ThingStatus.ONLINE); } @@ -198,7 +212,7 @@ protected void createChannel(String channelId, ChannelKind kind) { /** * Update channel value from {@link SensorConfig} object - override to include further channels - * + * * @param channelUID * @param newConfig */ @@ -222,7 +236,7 @@ protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { /** * Update channel value from {@link SensorState} object - override to include further channels - * + * * @param channelID * @param newState * @param initializing diff --git a/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/sensor-thing-types.xml b/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/sensor-thing-types.xml index 5e049e650ddbd..aaf15066691d5 100644 --- a/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/sensor-thing-types.xml +++ b/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/sensor-thing-types.xml @@ -13,6 +13,7 @@ + uid @@ -35,6 +36,14 @@ + + DateTime + + The date and time when the sensor was last seen. + Time + + + @@ -44,6 +53,7 @@ + uid @@ -84,6 +94,7 @@ + uid @@ -108,6 +119,7 @@ + uid @@ -168,6 +180,7 @@ + uid @@ -212,6 +225,7 @@ + uid @@ -235,6 +249,7 @@ + uid @@ -258,6 +273,7 @@ + uid @@ -317,6 +333,7 @@ + uid @@ -340,6 +357,7 @@ + uid @@ -363,6 +381,7 @@ + uid @@ -386,6 +405,7 @@ + uid @@ -416,6 +436,7 @@ + uid @@ -439,6 +460,7 @@ + uid @@ -461,6 +483,7 @@ + uid @@ -488,6 +511,7 @@ + uid From 19b5dd357e7b69297101d343c383d3d20d671727 Mon Sep 17 00:00:00 2001 From: Sami Salonen Date: Sat, 4 Jul 2020 13:55:38 +0300 Subject: [PATCH 30/85] [modbus] Update jamod to 1.2.4.OH (#8067) See https://github.com/openhab/jamod/pull/4 Signed-off-by: Sami Salonen --- bundles/org.openhab.binding.modbus/DEVELOPERS.md | 2 +- bundles/org.openhab.io.transport.modbus/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.modbus/DEVELOPERS.md b/bundles/org.openhab.binding.modbus/DEVELOPERS.md index 6ea2a20ec21a6..482f93e2569db 100644 --- a/bundles/org.openhab.binding.modbus/DEVELOPERS.md +++ b/bundles/org.openhab.binding.modbus/DEVELOPERS.md @@ -22,7 +22,7 @@ When configuring dependencies in `openhab-distro/launch/app/pom.xml`, add all de org.openhab.osgiify net.wimpi.jamod - 1.2.3.OH + 1.2.4.OH runtime ``` diff --git a/bundles/org.openhab.io.transport.modbus/pom.xml b/bundles/org.openhab.io.transport.modbus/pom.xml index 63f9ee5aa0efa..0f4beafe16284 100644 --- a/bundles/org.openhab.io.transport.modbus/pom.xml +++ b/bundles/org.openhab.io.transport.modbus/pom.xml @@ -27,7 +27,7 @@ net.wimpi jamod - 1.2.3.OH + 1.2.4.OH compile From eba4892685c4b95888f93db9722fceb839916512 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Sun, 5 Jul 2020 11:41:15 +0200 Subject: [PATCH 31/85] [kodi] Thing handler factory with null annotations and constructor injection (#8075) * [kodi] Thing handler factory with null annotations and constructor injection Signed-off-by: Laurent Garnier --- .../kodi/internal/KodiHandlerFactory.java | 70 +++++++------------ 1 file changed, 24 insertions(+), 46 deletions(-) diff --git a/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiHandlerFactory.java b/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiHandlerFactory.java index 141b7fb5744d9..0f7e209baf6bc 100644 --- a/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiHandlerFactory.java +++ b/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiHandlerFactory.java @@ -19,6 +19,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.websocket.client.WebSocketClient; import org.eclipse.smarthome.core.audio.AudioHTTPServer; import org.eclipse.smarthome.core.audio.AudioSink; @@ -33,6 +35,7 @@ import org.openhab.binding.kodi.internal.handler.KodiHandler; import org.osgi.framework.ServiceRegistration; import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; @@ -45,21 +48,32 @@ * @author Paul Frank - Initial contribution * @author Christoph Weitkamp - Improvements on channels for opening PVR TV or Radio streams */ +@NonNullByDefault @Component(service = ThingHandlerFactory.class, configurationPid = "binding.kodi") public class KodiHandlerFactory extends BaseThingHandlerFactory { - private Logger logger = LoggerFactory.getLogger(KodiHandlerFactory.class); + private final Logger logger = LoggerFactory.getLogger(KodiHandlerFactory.class); - private AudioHTTPServer audioHTTPServer; - private NetworkAddressService networkAddressService; - private WebSocketClient webSocketClient; + private final AudioHTTPServer audioHTTPServer; + private final NetworkAddressService networkAddressService; + private final KodiDynamicStateDescriptionProvider stateDescriptionProvider; + private final WebSocketClient webSocketClient; - // url (scheme+server+port) to use for playing notification sounds - private String callbackUrl = null; + private final Map> audioSinkRegistrations = new ConcurrentHashMap<>(); - private Map> audioSinkRegistrations = new ConcurrentHashMap<>(); + // url (scheme+server+port) to use for playing notification sounds + private @Nullable String callbackUrl; - private KodiDynamicStateDescriptionProvider stateDescriptionProvider; + @Activate + public KodiHandlerFactory(final @Reference AudioHTTPServer audioHTTPServer, + final @Reference NetworkAddressService networkAddressService, + final @Reference KodiDynamicStateDescriptionProvider stateDescriptionProvider, + final @Reference WebSocketFactory webSocketFactory) { + this.audioHTTPServer = audioHTTPServer; + this.networkAddressService = networkAddressService; + this.stateDescriptionProvider = stateDescriptionProvider; + this.webSocketClient = webSocketFactory.getCommonWebSocketClient(); + } @Override protected void activate(ComponentContext componentContext) { @@ -74,7 +88,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { } @Override - protected ThingHandler createHandler(Thing thing) { + protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (thingTypeUID.equals(THING_TYPE_KODI)) { @@ -94,7 +108,7 @@ protected ThingHandler createHandler(Thing thing) { return null; } - private String createCallbackUrl() { + private @Nullable String createCallbackUrl() { if (callbackUrl != null) { return callbackUrl; } else { @@ -123,40 +137,4 @@ public void unregisterHandler(Thing thing) { reg.unregister(); } } - - @Reference - protected void setHttpClientFactory(WebSocketFactory webSocketFactory) { - this.webSocketClient = webSocketFactory.getCommonWebSocketClient(); - } - - protected void unsetHttpClientFactory(WebSocketFactory webSocketFactory) { - this.webSocketClient = null; - } - - @Reference - protected void setAudioHTTPServer(AudioHTTPServer audioHTTPServer) { - this.audioHTTPServer = audioHTTPServer; - } - - protected void unsetAudioHTTPServer(AudioHTTPServer audioHTTPServer) { - this.audioHTTPServer = null; - } - - @Reference - protected void setNetworkAddressService(NetworkAddressService networkAddressService) { - this.networkAddressService = networkAddressService; - } - - protected void unsetNetworkAddressService(NetworkAddressService networkAddressService) { - this.networkAddressService = null; - } - - @Reference - protected void setDynamicStateDescriptionProvider(KodiDynamicStateDescriptionProvider stateDescriptionProvider) { - this.stateDescriptionProvider = stateDescriptionProvider; - } - - protected void unsetDynamicStateDescriptionProvider(KodiDynamicStateDescriptionProvider stateDescriptionProvider) { - this.stateDescriptionProvider = null; - } } From 3c5fb4292c9a24e35da2ba55dde2c3ea0b824283 Mon Sep 17 00:00:00 2001 From: toweosp <18066810+toweosp@users.noreply.github.com> Date: Sun, 5 Jul 2020 21:03:19 +0200 Subject: [PATCH 32/85] [lcn] Naming corrected (#8079) Signed-off-by: toweosp --- .../java/org/openhab/binding/lcn/internal/LcnModuleActions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleActions.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleActions.java index 2dd7524a44f8f..b76b44adb9a50 100644 --- a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleActions.java +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleActions.java @@ -172,7 +172,7 @@ private static ILcnModuleActions invokeMethodOf(@Nullable ThingActions actions) }); } } - throw new IllegalArgumentException("Actions is not an instance of EcobeeActions"); + throw new IllegalArgumentException("Actions is not an instance of LcnModuleActions"); } /** Static alias to support the old DSL rules engine and make the action available there. */ From e4b2e1c88da65664b2d4110c06caae532bccefdb Mon Sep 17 00:00:00 2001 From: Kai Neuhaus Date: Tue, 7 Jul 2020 19:17:24 +0200 Subject: [PATCH 33/85] [mqtt] Add warning for co-installation with mqtt1 (#8048) Signed-off-by: Kai Neuhaus --- bundles/org.openhab.binding.mqtt/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bundles/org.openhab.binding.mqtt/README.md b/bundles/org.openhab.binding.mqtt/README.md index 6fa804485baf9..389ee3cabbcc9 100644 --- a/bundles/org.openhab.binding.mqtt/README.md +++ b/bundles/org.openhab.binding.mqtt/README.md @@ -65,3 +65,7 @@ Configuration parameters are: * __stateTopic__: This channel will trigger on this MQTT topic. This topic can contain wildcards like + and # for example "all/in/#" or "sensors/+/config". * __payload__: An optional condition on the value of the MQTT topic that must match before this channel is triggered. +## Legacy MQTT1-Binding + +This binding is not supposed to run in parallel to the old mqtt1-binding. +Please uninstall the old binding before installing this binding. From d42ac14886f01361808615c9582f7320b393fb4e Mon Sep 17 00:00:00 2001 From: Bob A Date: Tue, 7 Jul 2020 14:46:37 -0400 Subject: [PATCH 34/85] [lutron] Add representation-property to thing definitions (#8084) - Add representation-property to thing definitions Signed-off-by: Bob Adair --- .../resources/ESH-INF/thing/thing-types.xml | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/bundles/org.openhab.binding.lutron/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.lutron/src/main/resources/ESH-INF/thing/thing-types.xml index e50555c415d03..ab9778f8e6689 100644 --- a/bundles/org.openhab.binding.lutron/src/main/resources/ESH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.lutron/src/main/resources/ESH-INF/thing/thing-types.xml @@ -11,6 +11,8 @@ Lutron + serialNumber + network-address @@ -67,6 +69,8 @@ + integrationId + @@ -95,6 +99,8 @@ + integrationId + @@ -117,6 +123,8 @@ + integrationId + @@ -146,6 +154,8 @@ + integrationId + @@ -166,6 +176,8 @@ + integrationId + @@ -201,6 +213,8 @@ + integrationId + @@ -226,6 +240,8 @@ + integrationId + @@ -246,6 +262,8 @@ + integrationId + @@ -262,6 +280,8 @@ Lutron seeTouch/Hybrid seeTouch wall keypad + integrationId + @@ -316,6 +336,8 @@ Lutron wireless tabletop keypad + integrationId + @@ -351,6 +373,8 @@ Lutron International seeTouch keypad (HomeWorks QS Only) + integrationId + @@ -388,6 +412,8 @@ Lutron Palladiom keypad (HomeWorks QS Only) + integrationId + @@ -428,6 +454,8 @@ Lutron wireless Pico keypad + integrationId + @@ -462,6 +490,8 @@ Lutron GRAFIK Eye QS for RadioRA 2/HomeWorks QS + integrationId + @@ -494,6 +524,8 @@ Lutron visor control receiver + integrationId + @@ -515,6 +547,8 @@ QS Wallbox Closure Interface + integrationId + @@ -537,6 +571,8 @@ Virtual integration keypad on repeater + integrationId + @@ -567,6 +603,8 @@ Lutron QS IO Interface + integrationId + @@ -604,6 +642,8 @@ + integrationId + @@ -624,6 +664,8 @@ + integrationId + From 8d585d8291bc53e62e34c26f23e185078c7da903 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Tue, 7 Jul 2020 20:49:33 +0200 Subject: [PATCH 35/85] [hue] Avoid updating the inbox at each poll (#8066) - Avoid updating the inbox at each poll Fix #8065 Signed-off-by: Laurent Garnier --- .../hue/internal/handler/HueBridgeHandler.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java index 22ca30517626b..5a23208090af2 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java @@ -176,13 +176,12 @@ protected void doConnectedRun() throws IOException, ApiException { for (final FullSensor sensor : hueBridge.getSensors()) { String sensorId = sensor.getId(); - lastSensorStateCopy.remove(sensorId); final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId); if (sensorStatusListener == null) { logger.debug("Hue sensor '{}' added.", sensorId); - if (discovery != null) { + if (discovery != null && !lastSensorStateCopy.containsKey(sensorId)) { discovery.addSensorDiscovery(sensor); } @@ -192,6 +191,7 @@ protected void doConnectedRun() throws IOException, ApiException { lastSensorStates.put(sensorId, sensor); } } + lastSensorStateCopy.remove(sensorId); } // Check for removed sensors @@ -227,13 +227,12 @@ protected void doConnectedRun() throws IOException, ApiException { for (final FullLight fullLight : lights) { final String lightId = fullLight.getId(); - lastLightStateCopy.remove(lightId); final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId); if (lightStatusListener == null) { logger.debug("Hue light '{}' added.", lightId); - if (discovery != null) { + if (discovery != null && !lastLightStateCopy.containsKey(lightId)) { discovery.addLightDiscovery(fullLight); } @@ -243,6 +242,7 @@ protected void doConnectedRun() throws IOException, ApiException { lastLightStates.put(lightId, fullLight); } } + lastLightStateCopy.remove(lightId); } // Check for removed lights @@ -310,14 +310,13 @@ protected void doConnectedRun() throws IOException, ApiException { groupState.getXY()); String groupId = fullGroup.getId(); - lastGroupStateCopy.remove(groupId); final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId); if (groupStatusListener == null) { logger.debug("Hue group '{}' ({}) added (nb lights {}).", groupId, fullGroup.getName(), fullGroup.getLights().size()); - if (discovery != null) { + if (discovery != null && !lastGroupStateCopy.containsKey(groupId)) { discovery.addGroupDiscovery(fullGroup); } @@ -327,6 +326,7 @@ protected void doConnectedRun() throws IOException, ApiException { lastGroupStates.put(groupId, fullGroup); } } + lastGroupStateCopy.remove(groupId); } // Check for removed groups From 20571d8f960c40142d90ed021aa72bea9c831745 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Tue, 7 Jul 2020 20:50:53 +0200 Subject: [PATCH 36/85] [openweathermap] Do not update the discovery inbox while the location is unchanged (#8078) - Do not update the discovery inbox while the location is unchanged Signed-off-by: Laurent Garnier --- .../discovery/OpenWeatherMapDiscoveryService.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/discovery/OpenWeatherMapDiscoveryService.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/discovery/OpenWeatherMapDiscoveryService.java index af48422cb3f65..ff9d880288156 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/discovery/OpenWeatherMapDiscoveryService.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/discovery/OpenWeatherMapDiscoveryService.java @@ -79,7 +79,7 @@ public void deactivate() { @Override protected void startScan() { logger.debug("Start manual OpenWeatherMap Location discovery scan."); - scanForNewLocation(); + scanForNewLocation(false); } @Override @@ -93,8 +93,9 @@ protected void startBackgroundDiscovery() { if (discoveryJob == null || discoveryJob.isCancelled()) { logger.debug("Start OpenWeatherMap Location background discovery job at interval {} s.", DISCOVERY_INTERVAL_SECONDS); - discoveryJob = scheduler.scheduleWithFixedDelay(this::scanForNewLocation, 0, DISCOVERY_INTERVAL_SECONDS, - TimeUnit.SECONDS); + discoveryJob = scheduler.scheduleWithFixedDelay(() -> { + scanForNewLocation(true); + }, 0, DISCOVERY_INTERVAL_SECONDS, TimeUnit.SECONDS); } } @@ -108,7 +109,7 @@ protected void stopBackgroundDiscovery() { } } - private void scanForNewLocation() { + private void scanForNewLocation(boolean updateOnlyIfNewLocation) { PointType currentLocation = locationProvider.getLocation(); if (currentLocation == null) { logger.debug("Location is not set -> Will not provide any discovery results."); @@ -117,7 +118,7 @@ private void scanForNewLocation() { currentLocation); createResults(currentLocation); previousLocation = currentLocation; - } else { + } else if (!updateOnlyIfNewLocation) { createResults(currentLocation); } } From dc3e8324f3ae5571c8ff0b68a8f0bafe316236e3 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Tue, 7 Jul 2020 21:00:02 +0200 Subject: [PATCH 37/85] [hue] Reduce the number of warnings (#8015) * Review comments considered * Last minor change Signed-off-by: Laurent Garnier --- .../hue/internal/config/HueBridgeConfig.java | 7 ++-- .../internal/handler/HueBridgeHandler.java | 29 +++++++------- .../hue/internal/handler/HueGroupHandler.java | 37 ++++++++--------- .../hue/internal/handler/HueLightHandler.java | 40 +++++++++---------- .../internal/handler/HueSensorHandler.java | 5 ++- 5 files changed, 57 insertions(+), 61 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/config/HueBridgeConfig.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/config/HueBridgeConfig.java index 59182703c9cba..edd1925d8400f 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/config/HueBridgeConfig.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/config/HueBridgeConfig.java @@ -26,14 +26,14 @@ public class HueBridgeConfig { public static final String HTTP = "http"; public static final String HTTPS = "https"; - private @NonNullByDefault({}) String ipAddress; + private @Nullable String ipAddress; private @Nullable Integer port; private String protocol = HTTP; private @Nullable String userName; private int pollingInterval = 10; private int sensorPollingInterval = 500; - public String getIpAddress() { + public @Nullable String getIpAddress() { return ipAddress; } @@ -42,7 +42,8 @@ public void setIpAddress(String ipAddress) { } public int getPort() { - return (port != null) ? port.intValue() : HTTPS.equals(protocol) ? 443 : 80; + Integer thePort = this.port; + return (thePort != null) ? thePort.intValue() : HTTPS.equals(protocol) ? 443 : 80; } public void setPort(int port) { diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java index 5a23208090af2..6ac9674ccb63f 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java @@ -89,14 +89,14 @@ public class HueBridgeHandler extends ConfigStatusBridgeHandler implements HueCl private final Logger logger = LoggerFactory.getLogger(HueBridgeHandler.class); - private final Map lastLightStates = new ConcurrentHashMap<>(); - private final Map lastSensorStates = new ConcurrentHashMap<>(); - private final Map lastGroupStates = new ConcurrentHashMap<>(); + private final Map lastLightStates = new ConcurrentHashMap<>(); + private final Map lastSensorStates = new ConcurrentHashMap<>(); + private final Map lastGroupStates = new ConcurrentHashMap<>(); private @Nullable HueLightDiscoveryService discoveryService; - private final Map lightStatusListeners = new ConcurrentHashMap<>(); - private final Map sensorStatusListeners = new ConcurrentHashMap<>(); - private final Map groupStatusListeners = new ConcurrentHashMap<>(); + private final Map lightStatusListeners = new ConcurrentHashMap<>(); + private final Map sensorStatusListeners = new ConcurrentHashMap<>(); + private final Map groupStatusListeners = new ConcurrentHashMap<>(); final ReentrantLock pollingLock = new ReentrantLock(); @@ -170,7 +170,7 @@ private boolean isReachable(String ipAddress) { private final Runnable sensorPollingRunnable = new PollingRunnable() { @Override protected void doConnectedRun() throws IOException, ApiException { - Map lastSensorStateCopy = new HashMap<>(lastSensorStates); + Map lastSensorStateCopy = new HashMap<>(lastSensorStates); final HueLightDiscoveryService discovery = discoveryService; @@ -204,7 +204,7 @@ protected void doConnectedRun() throws IOException, ApiException { sensorStatusListener.onSensorRemoved(); } - if (discovery != null) { + if (discovery != null && sensor != null) { discovery.removeSensorDiscovery(sensor); } }); @@ -214,7 +214,7 @@ protected void doConnectedRun() throws IOException, ApiException { private final Runnable lightPollingRunnable = new PollingRunnable() { @Override protected void doConnectedRun() throws IOException, ApiException { - Map lastLightStateCopy = new HashMap<>(lastLightStates); + Map lastLightStateCopy = new HashMap<>(lastLightStates); List lights; if (ApiVersionUtils.supportsFullLights(hueBridge.getVersion())) { @@ -255,12 +255,12 @@ protected void doConnectedRun() throws IOException, ApiException { lightStatusListener.onLightRemoved(); } - if (discovery != null) { + if (discovery != null && light != null) { discovery.removeLightDiscovery(light); } }); - Map lastGroupStateCopy = new HashMap<>(lastGroupStates); + Map lastGroupStateCopy = new HashMap<>(lastGroupStates); for (final FullGroup fullGroup : hueBridge.getGroups()) { State groupState = new State(); @@ -339,7 +339,7 @@ protected void doConnectedRun() throws IOException, ApiException { groupStatusListener.onGroupRemoved(); } - if (discovery != null) { + if (discovery != null && group != null) { discovery.removeGroupDiscovery(group); } }); @@ -917,7 +917,7 @@ public void startSearch(List serialNumbers) { }); } - private T withReAuthentication(String taskDescription, Callable runnable) { + private @Nullable T withReAuthentication(String taskDescription, Callable runnable) { if (hueBridge != null) { try { try { @@ -941,7 +941,8 @@ public Collection getConfigStatus() { Collection configStatusMessages; // Check whether an IP address is provided - if (hueBridgeConfig.getIpAddress() == null || hueBridgeConfig.getIpAddress().isEmpty()) { + String ip = hueBridgeConfig.getIpAddress(); + if (ip == null || ip.isEmpty()) { configStatusMessages = Collections.singletonList(ConfigStatusMessage.Builder.error(HOST) .withMessageKeySuffix(HueConfigStatusMessage.IP_ADDRESS_MISSING).withArguments(HOST).build()); } else { diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java index d176e6a1bc994..0e17f39d10987 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java @@ -135,8 +135,9 @@ public void dispose() { } ThingHandler handler = bridge.getHandler(); if (handler instanceof HueBridgeHandler) { - hueClient = (HueClient) handler; - hueClient.registerGroupStatusListener(this); + HueClient bridgeHandler = (HueClient) handler; + hueClient = bridgeHandler; + bridgeHandler.registerGroupStatusListener(this); } else { return null; } @@ -164,6 +165,7 @@ public void handleCommand(String channel, Command command, long fadeTime) { return; } + Integer lastColorTemp; StateUpdate groupState = null; switch (channel) { case CHANNEL_COLOR: @@ -174,15 +176,11 @@ public void handleCommand(String channel, Command command, long fadeTime) { } else { groupState = LightStateConverter.toColorLightState(hsbCommand, group.getState()); groupState.setOn(true); - if (groupState != null) { - groupState.setTransitionTime(fadeTime); - } + groupState.setTransitionTime(fadeTime); } } else if (command instanceof PercentType) { groupState = LightStateConverter.toBrightnessLightState((PercentType) command); - if (groupState != null) { - groupState.setTransitionTime(fadeTime); - } + groupState.setTransitionTime(fadeTime); } else if (command instanceof OnOffType) { groupState = LightStateConverter.toOnOffLightState((OnOffType) command); } else if (command instanceof IncreaseDecreaseType) { @@ -195,9 +193,7 @@ public void handleCommand(String channel, Command command, long fadeTime) { case CHANNEL_COLORTEMPERATURE: if (command instanceof PercentType) { groupState = LightStateConverter.toColorTemperatureLightState((PercentType) command); - if (groupState != null) { - groupState.setTransitionTime(fadeTime); - } + groupState.setTransitionTime(fadeTime); } else if (command instanceof OnOffType) { groupState = LightStateConverter.toOnOffLightState((OnOffType) command); } else if (command instanceof IncreaseDecreaseType) { @@ -210,9 +206,7 @@ public void handleCommand(String channel, Command command, long fadeTime) { case CHANNEL_BRIGHTNESS: if (command instanceof PercentType) { groupState = LightStateConverter.toBrightnessLightState((PercentType) command); - if (groupState != null) { - groupState.setTransitionTime(fadeTime); - } + groupState.setTransitionTime(fadeTime); } else if (command instanceof OnOffType) { groupState = LightStateConverter.toOnOffLightState((OnOffType) command); } else if (command instanceof IncreaseDecreaseType) { @@ -221,10 +215,11 @@ public void handleCommand(String channel, Command command, long fadeTime) { groupState.setTransitionTime(fadeTime); } } - if (groupState != null && lastSentColorTemp != null) { + lastColorTemp = lastSentColorTemp; + if (groupState != null && lastColorTemp != null) { // make sure that the light also has the latest color temp // this might not have been yet set in the light, if it was off - groupState.setColorTemperature(lastSentColorTemp); + groupState.setColorTemperature(lastColorTemp); groupState.setTransitionTime(fadeTime); } break; @@ -232,10 +227,11 @@ public void handleCommand(String channel, Command command, long fadeTime) { if (command instanceof OnOffType) { groupState = LightStateConverter.toOnOffLightState((OnOffType) command); } - if (groupState != null && lastSentColorTemp != null) { + lastColorTemp = lastSentColorTemp; + if (groupState != null && lastColorTemp != null) { // make sure that the light also has the latest color temp // this might not have been yet set in the light, if it was off - groupState.setColorTemperature(lastSentColorTemp); + groupState.setColorTemperature(lastColorTemp); groupState.setTransitionTime(fadeTime); } break; @@ -431,8 +427,9 @@ private void scheduleAlertStateRestore(Command command) { * restoration. */ private void cancelScheduledFuture() { - if (scheduledFuture != null) { - scheduledFuture.cancel(true); + ScheduledFuture scheduledJob = scheduledFuture; + if (scheduledJob != null) { + scheduledJob.cancel(true); scheduledFuture = null; } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java index a376766040208..4108284af5d4f 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java @@ -112,8 +112,7 @@ public class HueLightHandler extends BaseThingHandler implements LightStatusList private @Nullable HueClient hueClient; - @Nullable - ScheduledFuture scheduledFuture; + private @Nullable ScheduledFuture scheduledFuture; public HueLightHandler(Thing hueLight) { super(hueLight); @@ -231,14 +230,13 @@ public void handleCommand(String channel, Command command, long fadeTime) { return; } + Integer lastColorTemp; StateUpdate lightState = null; switch (channel) { case CHANNEL_COLORTEMPERATURE: if (command instanceof PercentType) { lightState = LightStateConverter.toColorTemperatureLightState((PercentType) command); - if (lightState != null) { - lightState.setTransitionTime(fadeTime); - } + lightState.setTransitionTime(fadeTime); } else if (command instanceof OnOffType) { lightState = LightStateConverter.toOnOffLightState((OnOffType) command); if (isOsramPar16) { @@ -255,9 +253,7 @@ public void handleCommand(String channel, Command command, long fadeTime) { case CHANNEL_BRIGHTNESS: if (command instanceof PercentType) { lightState = LightStateConverter.toBrightnessLightState((PercentType) command); - if (lightState != null) { - lightState.setTransitionTime(fadeTime); - } + lightState.setTransitionTime(fadeTime); } else if (command instanceof OnOffType) { lightState = LightStateConverter.toOnOffLightState((OnOffType) command); if (isOsramPar16) { @@ -269,10 +265,11 @@ public void handleCommand(String channel, Command command, long fadeTime) { lightState.setTransitionTime(fadeTime); } } - if (lightState != null && lastSentColorTemp != null) { + lastColorTemp = lastSentColorTemp; + if (lightState != null && lastColorTemp != null) { // make sure that the light also has the latest color temp // this might not have been yet set in the light, if it was off - lightState.setColorTemperature(lastSentColorTemp); + lightState.setColorTemperature(lastColorTemp); lightState.setTransitionTime(fadeTime); } break; @@ -284,10 +281,11 @@ public void handleCommand(String channel, Command command, long fadeTime) { lightState = addOsramSpecificCommands(lightState, (OnOffType) command); } } - if (lightState != null && lastSentColorTemp != null) { + lastColorTemp = lastSentColorTemp; + if (lightState != null && lastColorTemp != null) { // make sure that the light also has the latest color temp // this might not have been yet set in the light, if it was off - lightState.setColorTemperature(lastSentColorTemp); + lightState.setColorTemperature(lastColorTemp); lightState.setTransitionTime(fadeTime); } break; @@ -298,15 +296,11 @@ public void handleCommand(String channel, Command command, long fadeTime) { lightState = LightStateConverter.toOnOffLightState(OnOffType.OFF); } else { lightState = LightStateConverter.toColorLightState(hsbCommand, light.getState()); - if (lightState != null) { - lightState.setTransitionTime(fadeTime); - } + lightState.setTransitionTime(fadeTime); } } else if (command instanceof PercentType) { lightState = LightStateConverter.toBrightnessLightState((PercentType) command); - if (lightState != null) { - lightState.setTransitionTime(fadeTime); - } + lightState.setTransitionTime(fadeTime); } else if (command instanceof OnOffType) { lightState = LightStateConverter.toOnOffLightState((OnOffType) command); } else if (command instanceof IncreaseDecreaseType) { @@ -428,8 +422,9 @@ private StateUpdate createBrightnessStateUpdate(int currentBrightness, int newBr } ThingHandler handler = bridge.getHandler(); if (handler instanceof HueClient) { - hueClient = (HueClient) handler; - hueClient.registerLightStatusListener(this); + HueClient bridgeHandler = (HueClient) handler; + hueClient = bridgeHandler; + bridgeHandler.registerLightStatusListener(this); } else { return null; } @@ -576,8 +571,9 @@ private void scheduleAlertStateRestore(Command command) { * restoration. */ private void cancelScheduledFuture() { - if (scheduledFuture != null) { - scheduledFuture.cancel(true); + ScheduledFuture scheduledJob = scheduledFuture; + if (scheduledJob != null) { + scheduledJob.cancel(true); scheduledFuture = null; } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueSensorHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueSensorHandler.java index d8b876a51b4ff..e58673834de5a 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueSensorHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueSensorHandler.java @@ -149,8 +149,9 @@ public void dispose() { } ThingHandler handler = bridge.getHandler(); if (handler instanceof HueBridgeHandler) { - hueClient = (HueClient) handler; - hueClient.registerSensorStatusListener(this); + HueClient bridgeHandler = (HueClient) handler; + hueClient = bridgeHandler; + bridgeHandler.registerSensorStatusListener(this); } else { return null; } From 76cb28bc3493549c2fca6dd4b5392479b3f090a6 Mon Sep 17 00:00:00 2001 From: robnielsen Date: Tue, 7 Jul 2020 15:09:40 -0500 Subject: [PATCH 38/85] [insteon] added support for alternate heartbeat to motion sensor 2 (#8085) Signed-off-by: Rob Nielsen --- .../device/GroupMessageStateMachine.java | 14 +++++- .../internal/device/MessageHandler.java | 43 +++++++++++++++++++ .../src/main/resources/device_features.xml | 14 ++++++ .../src/main/resources/device_types.xml | 2 +- 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/GroupMessageStateMachine.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/GroupMessageStateMachine.java index 6e416a885d7d5..7782bc4f5f6d1 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/GroupMessageStateMachine.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/GroupMessageStateMachine.java @@ -118,6 +118,7 @@ enum State { */ public boolean action(GroupMessage a, InsteonAddress address, int group, byte cmd1) { publish = false; + long currentTime = System.currentTimeMillis(); switch (state) { case EXPECT_BCAST: switch (a) { @@ -136,7 +137,16 @@ public boolean action(GroupMessage a, InsteonAddress address, int group, byte cm switch (a) { case BCAST: if (lastCmd1 == cmd1) { - publish = false; + if (currentTime > lastUpdated + 30000) { + if (logger.isDebugEnabled()) { + logger.debug( + "{} group {} cmd1 {} is not a dup BCAST, received last message over 30000 ms ago", + address, group, Utils.getHexByte(cmd1)); + } + publish = true; + } else { + publish = false; + } } else { if (logger.isDebugEnabled()) { logger.debug("{} group {} cmd1 {} is not a dup BCAST, last cmd1 {}", address, group, @@ -181,7 +191,7 @@ public boolean action(GroupMessage a, InsteonAddress address, int group, byte cm } lastCmd1 = cmd1; - lastUpdated = System.currentTimeMillis(); + lastUpdated = currentTime; logger.debug("{} group {} state: {} --{}--> {}, publish: {}", address, group, oldState, a, state, publish); return (publish); } diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/MessageHandler.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/MessageHandler.java index 0b6028bdff07d..c4a7b54b7671d 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/MessageHandler.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/MessageHandler.java @@ -767,6 +767,49 @@ public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) { } } + @NonNullByDefault + public static class MotionSensor2AlternateHeartbeatHandler extends MessageHandler { + MotionSensor2AlternateHeartbeatHandler(DeviceFeature p) { + super(p); + } + + @Override + public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) { + InsteonDevice dev = f.getDevice(); + try { + // group 0x0B (11) - alternate heartbeat group + InsteonAddress toAddr = msg.getAddr("toAddress"); + int batteryLevel = toAddr.getHighByte() & 0xff; + int lightLevel = toAddr.getMiddleByte() & 0xff; + int temperatureLevel = msg.getByte("command2") & 0xff; + + logger.debug("{}: {} got light level: {}, battery level: {}, temperature level: {}", nm(), + dev.getAddress(), lightLevel, batteryLevel, temperatureLevel); + feature.publish(new DecimalType(lightLevel), StateChangeType.CHANGED, InsteonDeviceHandler.FIELD, + InsteonDeviceHandler.FIELD_LIGHT_LEVEL); + feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED, InsteonDeviceHandler.FIELD, + InsteonDeviceHandler.FIELD_BATTERY_LEVEL); + feature.publish(new DecimalType(temperatureLevel), StateChangeType.CHANGED, InsteonDeviceHandler.FIELD, + InsteonDeviceHandler.FIELD_TEMPERATURE_LEVEL); + + // per 2844-222 dev doc: working battery level range is 0xd2 - 0x70 + int batteryPercentage; + if (batteryLevel >= 0xd2) { + batteryPercentage = 100; + } else if (batteryLevel <= 0x70) { + batteryPercentage = 0; + } else { + batteryPercentage = (batteryLevel - 0x70) * 100 / (0xd2 - 0x70); + } + logger.debug("{}: {} battery percentage: {}", nm(), dev.getAddress(), batteryPercentage); + feature.publish(new QuantityType<>(batteryPercentage, SmartHomeUnits.PERCENT), StateChangeType.CHANGED, + InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_PERCENTAGE); + } catch (FieldException e) { + logger.warn("error parsing {}: ", msg, e); + } + } + } + @NonNullByDefault public static class HiddenDoorSensorDataReplyHandler extends MessageHandler { HiddenDoorSensorDataReplyHandler(DeviceFeature p) { diff --git a/bundles/org.openhab.binding.insteon/src/main/resources/device_features.xml b/bundles/org.openhab.binding.insteon/src/main/resources/device_features.xml index 5e42b34c691db..1054e85316352 100644 --- a/bundles/org.openhab.binding.insteon/src/main/resources/device_features.xml +++ b/bundles/org.openhab.binding.insteon/src/main/resources/device_features.xml @@ -442,6 +442,20 @@ NoOpCommandHandler NoPollHandler + + SimpleDispatcher + NoOpMsgHandler + NoOpMsgHandler + NoOpMsgHandler + NoOpMsgHandler + NoOpMsgHandler + MotionSensor2AlternateHeartbeatHandler + NoOpMsgHandler + NoOpMsgHandler + MotionSensorDataReplyHandler + NoOpCommandHandler + NoPollHandler + SimpleDispatcher NoOpMsgHandler diff --git a/bundles/org.openhab.binding.insteon/src/main/resources/device_types.xml b/bundles/org.openhab.binding.insteon/src/main/resources/device_types.xml index 7775b88f8facf..2d50b075f0d14 100644 --- a/bundles/org.openhab.binding.insteon/src/main/resources/device_types.xml +++ b/bundles/org.openhab.binding.insteon/src/main/resources/device_types.xml @@ -591,7 +591,7 @@ WirelessMotionSensorContact WirelessMotionSensorLightLevelAboveThreshold WirelessMotionSensorLowBattery - MotionSensorData + MotionSensor2Data WirelessMotionSensor2TamperSwitch GenericLastTime From 8fe3e73e1e4263cd869e20b62ce06adec3cbf703 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Tue, 7 Jul 2020 22:43:52 +0200 Subject: [PATCH 39/85] [amazonechocontrol] add support for smarthome devices (#7969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix json parsing Improve error handling Remove unused Exception * Changes for new build system * Original * Bugfixed for new version and merge request * Added license information and author * Added contributor and readme information * Uncommented test comment for production use * Removed the waitForUpdate variable - not used * Added two configurations params for pulling interval and activating the smart bulbs * [amazonechocontrol] Formatting change Fix for Announcment Channel * Bugfixed discovery * Bugfixed wrong entries of the amazon echo devices as lights * [amazonechocontrol] Fix for Announcment Channel * Implemented recursive searching for devices, filtered smart plugs * Added smart plugs * Removed unused code, renamed new function and it's references * Added translation * Added documentation * Added capabilities to smart home device * Added dynamic channel adding Improve smart home discover and and add options for openHAB smarthome devices Handle on/off in percentage channel Smart Home Device Handling Update Smart Home Device Handling Move Polling Timer to Account Handler Remove polling from smart home handler Alexa guard support * [amazonechocontrol] Bugfix for login in Australia (#6034) Fix invalid device state Bugfix and docu for announcment Fix duplicate channel registration Fix updates Skill device detection Improve channel handling Color Name handling Single device update Color handling Handle Security Panel Controller Color handling Alexa.AcousticEventSensor added Code cleanup Alexa.TemperatureSensor Interface PowerLevelController, PercentageController Add readme Add To Do List Fix nullable warning in Announcment Fix nullable warning Prepare Release Notes SmartHome update groups Fix spelling Fix group initialization * Removed unused file * Removed unused file * Error fixes * codestyle * Fix issues, codestyle and refactoring * address review comment Co-authored-by: Lkn94 Co-authored-by: Michael Geramb Co-authored-by: Lukas Knöller Signed-off-by: Jan N. Klug --- .../README.md | 305 ++++- .../internal/AccountHandlerConfig.java | 28 + .../internal/AccountServlet.java | 76 +- .../AmazonEchoControlBindingConstants.java | 231 ++-- .../AmazonEchoControlHandlerFactory.java | 334 +++--- ...onEchoDynamicStateDescriptionProvider.java | 54 +- .../internal/Connection.java | 320 ++++-- .../internal/WebSocketConnection.java | 6 +- .../channelhandler/ChannelHandler.java | 2 + .../ChannelHandlerAnnouncement.java | 8 +- .../ChannelHandlerSendMessage.java | 16 +- .../discovery/AmazonEchoDiscovery.java | 77 +- .../discovery/SmartHomeDevicesDiscovery.java | 247 ++++ .../internal/handler/AccountHandler.java | 359 ++++-- .../internal/handler/EchoHandler.java | 110 +- .../handler/SmartHomeDeviceHandler.java | 388 +++++++ .../internal/jsons/JsonActivities.java | 11 +- .../jsons/JsonAnnouncementTarget.java | 3 +- .../internal/jsons/JsonAscendingAlarm.java | 2 +- .../internal/jsons/JsonColorTemperature.java | 28 + .../internal/jsons/JsonColors.java | 28 + .../jsons/JsonCommandPayloadPushActivity.java | 2 +- .../jsons/JsonCommandPayloadPushDevice.java | 2 +- .../jsons/JsonDeviceNotificationState.java | 2 +- .../jsons/JsonExchangeTokenResponse.java | 20 +- .../internal/jsons/JsonNetworkDetails.java | 26 + .../jsons/JsonRegisterAppRequest.java | 65 +- .../jsons/JsonRegisterAppResponse.java | 111 +- .../jsons/JsonRenewTokenResponse.java | 11 +- .../jsons/JsonSmartHomeCapabilities.java | 41 + .../jsons/JsonSmartHomeDeviceAlias.java | 33 + .../JsonSmartHomeDeviceNetworkState.java | 30 + .../internal/jsons/JsonSmartHomeDevices.java | 63 ++ .../jsons/JsonSmartHomeGroupIdentifiers.java | 29 + .../jsons/JsonSmartHomeGroupIdentity.java | 30 + .../internal/jsons/JsonSmartHomeGroups.java | 52 + .../internal/jsons/JsonSmartHomeTags.java | 29 + .../internal/jsons/JsonTokenResponse.java | 17 +- .../internal/jsons/JsonUsersMeResponse.java | 35 +- .../internal/jsons/JsonWebSiteCookie.java | 14 +- .../internal/jsons/SmartHomeBaseDevice.java | 29 + .../internal/smarthome/Constants.java | 58 + .../DynamicStateDescriptionSmartHome.java | 81 ++ .../smarthome/HandlerAcousticEventSensor.java | 113 ++ .../internal/smarthome/HandlerBase.java | 140 +++ .../HandlerBrightnessController.java | 143 +++ .../smarthome/HandlerColorController.java | 161 +++ .../HandlerColorTemperatureController.java | 162 +++ .../HandlerPercentageController.java | 142 +++ .../smarthome/HandlerPowerController.java | 109 ++ .../HandlerPowerLevelController.java | 144 +++ .../HandlerSecurityPanelController.java | 171 +++ .../smarthome/HandlerTemperatureSensor.java | 98 ++ ...tHomeDeviceStateGroupUpdateCalculator.java | 129 +++ .../resources/ESH-INF/binding/binding.xml | 2 +- .../i18n/amazonechocontrol_de.properties | 61 + .../resources/ESH-INF/thing/thing-types.xml | 1001 +++++++++-------- 57 files changed, 4751 insertions(+), 1238 deletions(-) create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountHandlerConfig.java rename bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/{statedescription => }/AmazonEchoDynamicStateDescriptionProvider.java (85%) create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonColorTemperature.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonColors.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeCapabilities.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDeviceAlias.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDeviceNetworkState.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevices.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroupIdentifiers.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroupIdentity.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroups.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeTags.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/SmartHomeBaseDevice.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/Constants.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/DynamicStateDescriptionSmartHome.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerAcousticEventSensor.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBase.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBrightnessController.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorTemperatureController.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPercentageController.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerController.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerLevelController.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerTemperatureSensor.java create mode 100644 bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/SmartHomeDeviceStateGroupUpdateCalculator.java diff --git a/bundles/org.openhab.binding.amazonechocontrol/README.md b/bundles/org.openhab.binding.amazonechocontrol/README.md index 36539453468be..c5ff4f32a55ad 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/README.md +++ b/bundles/org.openhab.binding.amazonechocontrol/README.md @@ -27,6 +27,20 @@ It provides features to control and view the current state of echo devices: - get information about the next alarm, reminder and timer - send a message to the echo devices +It also provides features to control devices connected to your echo: + +- turn on/off your lights +- change the color +- control groups of lights or just single bulbs +- receive the current state of the lights +- turn on/off smart plugs (e. g. OSRAM) + +Restrictions: + +- groups can't receive their current color (multiple colors are possible) +- devices can only receive their state every 60 seconds +- turning on/off (or switch color, change brightness) will send a request for every single bulb in a group + Some ideas what you can do in your home by using rules and other openHAB controlled devices: - Automatic turn on your amplifier and connect echo with bluetooth if the echo plays music @@ -41,26 +55,21 @@ Some ideas what you can do in your home by using rules and other openHAB control - Let Alexa say 'welcome' to you if you open the door - Implement own handling for voice commands in a rule - Change the equalizer settings depending on the bluetooth connection -- Turn on a light on your Alexa alarm time - -## Note +- Turn on a light on your alexa alarm time +- Activate or deactivate the Alexa Guard with presence detection -This binding uses the same API as the Web-Browser-Based Alexa site (alexa.amazon.de). -In other words, it simulates a user which is using the web page. -Unfortunately, the binding can get broken if Amazon change the web site. +With the possibility to control your lights you could do: -The binding is tested with amazon.de, amazon.fr, amazon.it, amazon.com and amazon.co.uk accounts, but should also work with all others. +- a scene-based configuration of your rooms +- connect single bulbs to functions of openhab +- simulate your presence at home +- automatically turn on your lights at the evening +- integrate your smart bulbs with rules -## Supported Things +## Binding Configuration -| Thing type id | Name | -|----------------------|---------------------------------------| -| account | Amazon Account | -| echo | Amazon Echo Device | -| echospot | Amazon Echo Spot Device | -| echoshow | Amazon Echo Show Device | -| wha | Amazon Echo Whole House Audio Control | -| flashbriefingprofile | Flash briefing profile | +The binding does not have any configuration. +The configuration of your amazon account must be done in the 'Amazon Account' device. ## First Steps @@ -75,18 +84,52 @@ If the device type is not known by the binding, the device will not be discovere But you can define any device listed in your Alexa app with the best matching existing device (e.g. echo). You will find the required serial number in settings of the device in the Alexa app. -## Binding Configuration +### Discover Smart Home Devices + +If you want to discover your smart home devices you need to activate it in the 'Amazon Account' thing. +Devices from other skills can be discovered too. +See section *Smart Home Devices* below for more information. + +## Account -The binding does not have any configuration. The configuration of your Amazon account must be done in the 'Amazon Account' device. -## Thing Configuration +### Account Thing + +#### Supported Thing Type + +| Thing type id | Name | +|----------------------|---------------------------------------| +| account | Amazon Account | + +#### Thing Configuration -The Amazon Account does not need any configuration. +| Configuration name | Default | Description | +|---------------------------------|---------|---------------------------------------------------------------------------------------| +| discoverSmartHome | 0 | 0...No discover, 1...Discover direct connected, 2...Discover direct and Alexa skill devices, 3...Discover direct, Alexa and openHAB skill devices | +| pollingIntervalSmartHomeAlexa | 30 | Defines the time in seconds for openHAB to pull the state of the Alexa connected devices. The minimum is 10 seconds. | +| pollingIntervalSmartSkills | 120 | Defines the time in seconds for openHAB to pull the state of the over a skill connected devices. The minimum is 60 seconds. | -### Amazon Devices +#### Channels -All Amazon devices (echo, echospot, echoshow, wha) needs the following configurations: +| Channel Type ID | Item Type | Access Mode | Thing Type | Description +|-----------------------|-------------|-------------|-------------------------------|------------------------------------------------------------------------------------------ +| sendMessage | String | W | account | Write Only! Sends a message to the Echo devices. + +## Echo Control (Control Echo devices from openHAB) + +### echo, echospot, echoshow, wha Things + +#### Supported Thing Type + +| Thing type id | Name | +|----------------------|---------------------------------------| +| echo | Amazon Echo Device | +| echospot | Amazon Echo Spot Device | +| echoshow | Amazon Echo Show Device | +| wha | Amazon Echo Whole House Audio Control | + +#### Thing Configuration | Configuration name | Description | |--------------------------|----------------------------------------------------| @@ -99,7 +142,7 @@ You will find the serial number in the Alexa app or on the webpage YOUR_OPENHAB/ The flashbriefingprofile thing has no configuration parameters. It will be configured at runtime by using the save channel to store the current flash briefing configuration in the thing. -## Channels +#### Channels | Channel Type ID | Item Type | Access Mode | Thing Type | Description |-----------------------|-------------|-------------|-------------------------------|------------------------------------------------------------------------------------------ @@ -124,7 +167,7 @@ It will be configured at runtime by using the save channel to store the current | amazonMusic | Switch | R/W | echo, echoshow, echospot, wha | Start playing of the last used Amazon Music song (works after at least one song was started after the openhab start) | remind | String | R/W | echo, echoshow, echospot | Write Only! Speak the reminder and sends a notification to the Alexa app (Currently the reminder is played and notified two times, this seems to be a bug in the Amazon software) | nextReminder | DateTime | R | echo, echoshow, echospot | Next reminder on the device -| playAlarmSound | String | W | echo, echoshow, echospot | Write Only! Plays ans Alarm sound +| playAlarmSound | String | W | echo, echoshow, echospot | Write Only! Plays an Alarm sound | nextAlarm | DateTime | R | echo, echoshow, echospot | Next alarm on the device | nextMusicAlarm | DateTime | R | echo, echoshow, echospot | Next music alarm on the device | nextTimer | DateTime | R | echo, echoshow, echospot | Next timer on the device @@ -132,8 +175,8 @@ It will be configured at runtime by using the save channel to store the current | musicProviderId | String | R/W | echo, echoshow, echospot | Current Music provider | playMusicVoiceCommand | String | W | echo, echoshow, echospot | Write Only! Voice command as text. E.g. 'Yesterday from the Beatles' | startCommand | String | W | echo, echoshow, echospot | Write Only! Used to start anything. Available options: Weather, Traffic, GoodMorning, SingASong, TellStory, FlashBriefing and FlashBriefing. (Note: The options are case sensitive) -| announcement | String | W | echo, echoshow, echospot | Write Only! Display the announcement message on the display. Please note: the announcement feature must be activated in the alexa app at the echo device. See in the tutorial section to learn how it’s possible to set the title and turn off the sound. -| textToSpeech | String | W | echo, echoshow, echospot | Write Only! Write some text to this channel and Alexa will speak it. It is possible to use plain text or SSML: e.g. `I want to tell you a secret.I am not a real human. Please note: the announcement feature must be activated in the alexa app at the echo device to use SSML. ` +| announcement | String | W | echo, echoshow, echospot | Write Only! Display the announcement message on the display. See in the tutorial section to learn how it’s possible to set the title and turn off the sound. +| textToSpeech | String | W | echo, echoshow, echospot | Write Only! Write some text to this channel and Alexa will speak it. It is possible to use plain text or SSML: e.g. `I want to tell you a secret.I am not a real human.` | textToSpeechVolume | Dimmer | R/W | echo, echoshow, echospot | Volume of the textToSpeech channel, if 0 the current volume will be used | lastVoiceCommand | String | R/W | echo, echoshow, echospot | Last voice command spoken to the device. Writing to the channel starts voice output. | mediaProgress | Dimmer | R/W | echo, echoshow, echospot | Media progress in percent @@ -156,25 +199,21 @@ E.g. to read out the history call from an installation on openhab:8080 with an a http://openhab:8080/amazonechocontrol/account1/PROXY/api/activities?startTime=&size=50&offset=1 -## Full Example +### Example -### amazonechocontrol.things +#### echo.things ``` -Bridge amazonechocontrol:account:account1 "Amazon Account" @ "Accounts" +Bridge amazonechocontrol:account:account1 "Amazon Account" @ "Accounts" [discoverSmartHome=2, pollingIntervalSmartHomeAlexa=30, pollingIntervalSmartSkills=120] { Thing echo echo1 "Alexa" @ "Living Room" [serialNumber="SERIAL_NUMBER"] Thing echoshow echoshow1 "Alexa" @ "Kitchen" [serialNumber="SERIAL_NUMBER"] Thing echospot echospot1 "Alexa" @ "Sleeping Room" [serialNumber="SERIAL_NUMBER"] Thing wha wha1 "Ground Floor Music Group" @ "Music Groups" [serialNumber="SERIAL_NUMBER"] - Thing flashbriefingprofile flashbriefing1 "Flash Briefing Technical" @ "Flash Briefings" - Thing flashbriefingprofile flashbriefing2 "Flash Briefing Life Style" @ "Flash Briefings" } ``` -You will find the serial number in the Alexa app. - -### amazonechocontrol.items: +#### echo.items: Sample for the Thing echo1 only. But it will work in the same way for the other things, only replace the thing name in the channel link. Take a look in the channel description above to know, which channels are supported by your thing type. @@ -222,6 +261,21 @@ Switch Echo_Living_Room_Bluetooth "Bluetooth" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothDeviceName"} // Commands +String Echo_Living_Room_Announcement "Announcement" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:announcement"} +String Echo_Living_Room_TTS "Text to Speech" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:textToSpeech"} +Dimmer Echo_Living_Room_TTS_Volume "Text to Speech Volume" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:textToSpeechVolume"} +String Echo_Living_Room_Remind "Remind" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:remind"} +String Echo_Living_Room_PlayAlarmSound "Play Alarm Sound" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playAlarmSound"} +String Echo_Living_Room_StartRoutine "Start Routine" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:startRoutine"} +Dimmer Echo_Living_Room_NotificationVolume "Notification volume" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:notificationVolume"} +Switch Echo_Living_Room_AscendingAlarm "Ascending alarm" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:ascendingAlarm"} + +// Feedbacks +String Echo_Living_Room_LastVoiceCommand "Last voice command" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:lastVoiceCommand"} +DateTime Echo_Living_Room_NextReminder "Next reminder" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:nextReminder"} +DateTime Echo_Living_Room_NextAlarm "Next alarm" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:nextAlarm"} +DateTime Echo_Living_Room_NextMusicAlarm "Next music alarm" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:nextMusicAlarm"} +DateTime Echo_Living_Room_NextTimer "Next timer" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:nextTimer"} String Echo_Living_Room_Announcement "Announcement" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:announcement"} String Echo_Living_Room_TTS "Text to Speech" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:textToSpeech"} Dimmer Echo_Living_Room_TTS_Volume "Text to Speech Volume" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:textToSpeechVolume"} @@ -238,7 +292,6 @@ DateTime Echo_Living_Room_NextAlarm "Next alarm" DateTime Echo_Living_Room_NextMusicAlarm "Next music alarm" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:nextMusicAlarm"} DateTime Echo_Living_Room_NextTimer "Next timer" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:nextTimer"} - // Flashbriefings Switch FlashBriefing_Technical_Save "Save (Write only)" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing1:save"} Switch FlashBriefing_Technical_Active "Active" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing1:active"} @@ -249,7 +302,7 @@ Switch FlashBriefing_LifeStyle_Active "Active" { channel="amazonechoc String FlashBriefing_LifeStyle_Play "Play (Write only)" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing2:playOnDevice"} ``` -### amazonechocontrol.sitemap: +#### echo.sitemap: ``` sitemap amazonechocontrol label="Echo Devices" @@ -299,7 +352,62 @@ sitemap amazonechocontrol label="Echo Devices" Slider item=Echo_Living_Room_NotificationVolume Switch item=Echo_Living_Room_AscendingAlarm } +} +``` + +## Flash Briefing + +#### Supported Things + +| Thing type id | Name | +|----------------------|---------------------------------------| +| flashbriefingprofile | Flash briefing profile | + +#### Channels + +The flashbriefingprofile thing has no configuration parameters. +It will be configured at runtime by using the save channel to store the current flash briefing configuration which is set in the alexa app in the thing. Create a flashbriefingprofile Thing for each set you need. +E.g. One Flashbriefing profile with technical news and wheater, one for playing world news and one for sport news. + +| Channel Type ID | Item Type | Access Mode | Thing Type | Description +|-----------------------|-------------|-------------|-------------------------------|------------------------------------------------------------------------------------------ +| save | Switch | W | flashbriefingprofile | Write Only! Stores the current configuration of flash briefings within the thing +| active | Switch | R/W | flashbriefingprofile | Active the profile +| playOnDevice | String | W | flashbriefingprofile | Specify the echo serial number or name to start the flash briefing. + +#### Example + +#### flashbriefings.things + +``` +Bridge amazonechocontrol:account:account1 "Amazon Account" @ "Accounts" [discoverSmartHome=2] +{ + Thing flashbriefingprofile flashbriefing1 "Flash Briefing Technical" @ "Flash Briefings" + Thing flashbriefingprofile flashbriefing2 "Flash Briefing Life Style" @ "Flash Briefings" +} +``` + +#### flashbriefings.items: + +Sample for the Thing echo1 only. But it will work in the same way for the other things, only replace the thing name in the channel link. +Take a look in the channel description above to know, which channels are supported by your thing type. + +``` +// Flashbriefings +Switch FlashBriefing_Technical_Save "Save (Write only)" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing1:save"} +Switch FlashBriefing_Technical_Active "Active" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing1:active"} +String FlashBriefing_Technical_Play "Play (Write only)" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing1:playOnDevice"} + +Switch FlashBriefing_LifeStyle_Save "Save (Write only)" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing2:save"} +Switch FlashBriefing_LifeStyle_Active "Active" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing2:active"} +String FlashBriefing_LifeStyle_Play "Play (Write only)" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing2:playOnDevice"} +``` +#### flashbriefings.sitemap: + +``` +sitemap flashbriefings label="Flash Briefings" +{ Frame label="Flash Briefing Technical" { Switch item=FlashBriefing_Technical_Save Switch item=FlashBriefing_Technical_Active @@ -314,6 +422,104 @@ sitemap amazonechocontrol label="Echo Devices" } ``` +## Smart Home Devices + +Note: the cannels of smartHomeDevices and smartHomeDeviceGroup will be created dynamically based on the capabilities reported by the amazon server. This can take a little bit of time. +The polling interval configured in the Account Thing to get the state is specified in minutes and has a minimum of 10. This means it takes up to 10 minutes to see the state of a channel. The reason for this low interval is, that the polling causes a big server load for the Smart Home Skills. + +#### Supported Things + +| Thing type id | Name | +|----------------------|---------------------------------------| +| smartHomeDevice | Smart Home Device | +| smartHomeDeviceGroup | Smart Home Device group | + + +#### Thing configuration of smartHomeDevice, smartHomeDeviceGroup + +| Configuration name | Description | +|--------------------------|---------------------------------------------------------------------------| +| id | The id of the device or device group | + +The only possibility to find out the id is by using the discover function in the PaperUI. You can use then the id, if you want define the Thing in a file. + +#### Channels + +The channels of the smarthome devices will be generated at runtime. Check in the paperUI thing configurations, which channels are created. + +| Channel Type ID | Item Type | Access Mode | Thing Type | Description +|--------------------------|-----------|-------------|-------------------------------|------------------------------------------------------------------------------------------ +| powerState | Switch | R/W | smartHomeDevice, smartHomeDeviceGroup | Shows and changes the state (ON/OFF) of your device +| brightness | Dimmer | R/W | smartHomeDevice, smartHomeDeviceGroup | Shows and changes the brightness of your lamp +| color | Color | R | smartHomeDevice, smartHomeDeviceGroup | Shows the color of your light +| colorName | String | R/W | smartHomeDevice, smartHomeDeviceGroup | Shows and changes the color name of your light (groups are not able to show their color) +| colorTemperatureName | String | R/W | smartHomeDevice, smartHomeDeviceGroup | White temperatures name of your lights (groups are not able to show their color) +| armState | String | R/W | smartHomeDevice, smartHomeDeviceGroup | State of your alarm guard. Options: ARMED_AWAY, ARMED_STAY, ARMED_NIGHT, DISARMED (groups are not able to show their state) +| burglaryAlarm | Contact | R | smartHomeDevice | Burglary alarm +| carbonMonoxideAlarm | Contact | R | smartHomeDevice | Carbon monoxide detection alarm +| fireAlarm | Contact | R | smartHomeDevice | Fire alarm +| waterAlarm | Contact | R | smartHomeDevice | Water alarm +| glassBreakDetectionState | Contact | R | smartHomeDevice | Glass break detection alarm +| smokeAlarmDetectionState | Contact | R | smartHomeDevice | Smoke detection alarm +| temperature | Number | R | smartHomeDevice | Temperature + +### Example + +#### smarthome.things + +``` +Bridge amazonechocontrol:account:account1 "Amazon Account" @ "Accounts" [discoverSmartHome=2, pollingIntervalSmartHomeAlexa=30, pollingIntervalSmartSkills=120] +{ + Thing smartHomeDevice smartHomeDevice1 "Smart Home Device 1" @ "Living Room" [id="ID"] + Thing smartHomeDevice smartHomeDevice2 "Smart Home Device 2" @ "Living Room" [id="ID"] + Thing smartHomeDevice smartHomeDevice3 "Smart Home Device 3" @ "Living Room" [id="ID"] + Thing smartHomeDeviceGroup smartHomeDeviceGroup1 "Living Room Group" @ "Living Room" [id="ID"] +} +``` + +#### smarthome.items: + +Sample for the Thing echo1 only. But it will work in the same way for the other things, only replace the thing name in the channel link. +Take a look in the channel description above to know which channels are supported by your thing type. + +``` +// Lights and lightgroups +Switch Light_State "On/Off" { channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:powerState" } +Dimmer Light_Brightness "Brightness" { channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:brightness" } +Color Light_Color "Color" { channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:color" } +String Light_Color_Name "Color Name" { channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:colorName" } +String Light_White "White temperature" { channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:colorTemperatureName" } + +// Smart plugs +Switch Plug_State "On/Off" { channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice2:powerState" } + +// Alexa Guard +Switch Arm_State "State" { channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice3:armState" } + +// Smart Home device group +Switch Group_State "On/Off" { channel="amazonechocontrol:smartHomeDeviceGroup:account1:smartHomeDeviceGroup1:powerState" } +``` + +The only possibility to find out the id for the smartHomeDevice and smartHomeDeviceGroup Things is by using the discover function in the PaperUI. + +#### smarthome.sitemap: + +``` +sitemap smarthome label="Smart Home Devices" +{ + Frame label="Lights and light groups" { + Switch item=Light_State + Slider item=Light_Brightness + Default item=Light_Color + Selection item=Light_Color_Name mappings=[ ''='', 'red'='Red', 'crimson'='Crimson', 'salmon'='Salmon', 'orange'='Orange', 'gold'='Gold', 'yellow'='Yellow', 'green'='Green', 'turquoise'='Turquoise', 'cyan'='Cyan', 'sky_blue'='Sky Blue', 'blue'='Blue', 'purple'='Purple', 'magenta'='Magenta', 'pink'='Pink', 'lavender'='Lavender' ] + Selection item=Light_White mappings=[ ''='', 'warm_white'='Warm white', 'soft_white'='Soft white', 'white'='White', 'daylight_white'='Daylight white', 'cool_white'='Cool white' ] + Switch item=Light_State + Switch item=Group_State + Selection item=Arm_State mappings=[ 'ARMED_AWAY'='Active', 'ARMED_STAY'='Present', 'ARMED_NIGHT'='Night', 'DISARMED'='Deactivated' ] + } +} +``` + ## How To Get IDs 1) Open the url YOUR_OPENHAB/amazonechocontrol in your browser (e.g. http://openhab:8080/amazonechocontrol/) @@ -321,6 +527,14 @@ sitemap amazonechocontrol label="Echo Devices" 3) Click on the name of the echo thing 4) Scroll to the channel and copy the required ID +## Advanced Feature Technically Experienced Users + +The url /amazonechocontrol//PROXY/ provides a proxy server with an authenticated connection to the amazon alexa server. This can be used to call alexa api from rules. + +E.g. to read out the history call from an installation on openhab:8080 with a account named account1: + +http://openhab:8080/amazonechocontrol/account1/PROXY/api/activities?startTime=&size=50&offset=1 + ## Tutorials ### Let Alexa speak a text from a rule: @@ -363,6 +577,7 @@ end ``` Expert: + You can use a json formatted string to control title, sound and volume: ```php @@ -377,6 +592,7 @@ No specification uses the volume from the `textToSpeechVolume` channel. Note: If you turn off the sound and Alexa is playing music, it will anyway turn down the volume for a moment. This behavior can not be changed. + ```php rule "Say welcome if the door opens" when @@ -463,6 +679,23 @@ then end ``` +## Advanced Feature Technically Experienced Users + +The url /amazonechocontrol//PROXY/ provides a proxy server with an authenticated connection to the Amazon Alexa server. +This can be used to call Alexa API from rules. + +E.g. to read out the history call from an installation on openhab:8080 with an account named account1: + +http://openhab:8080/amazonechocontrol/account1/PROXY/api/activities?startTime=&size=50&offset=1 + +## Note + +This binding uses the same API as the Web-Browser-Based Alexa site (alexa.amazon.de). +In other words, it simulates a user which is using the web page. +Unfortunately, this binding can break if Amazon changes the web site. + +The binding is tested with amazon.de, amazon.fr, amazon.it, amazon.com and amazon.co.uk accounts, but should also work with all others. + ## Credits The idea for writing this binding came from this blog: https://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html (German). diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountHandlerConfig.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountHandlerConfig.java new file mode 100644 index 0000000000000..f5476e352cc15 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountHandlerConfig.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler; + +/** + * The {@link AccountHandlerConfig} holds the configuration for the {@link AccountHandler} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class AccountHandlerConfig { + public int discoverSmartHome = 0; + public int pollingIntervalSmartHomeAlexa = 60; + public int pollingIntervalSmartSkills = 120; +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java index 41d4bceae4255..27ff048879868 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java @@ -33,7 +33,6 @@ import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Thing; @@ -55,7 +54,6 @@ import com.google.gson.JsonSyntaxException; /** - * * Provides the following functions * --- Login --- * Simple http proxy to forward the login dialog from amazon to the user through the binding @@ -76,33 +74,27 @@ public class AccountServlet extends HttpServlet { private final Logger logger = LoggerFactory.getLogger(AccountServlet.class); - final HttpService httpService; - String servletUrlWithoutRoot; - final String servletUrl; - AccountHandler account; - String id; - @Nullable - Connection connectionToInitialize; - final Gson gson; + private final HttpService httpService; + private final String servletUrlWithoutRoot; + private final String servletUrl; + private final AccountHandler account; + private final String id; + private @Nullable Connection connectionToInitialize; + private final Gson gson; public AccountServlet(HttpService httpService, String id, AccountHandler account, Gson gson) { this.httpService = httpService; this.account = account; this.id = id; this.gson = gson; + try { servletUrlWithoutRoot = "amazonechocontrol/" + URLEncoder.encode(id, "UTF8"); - } catch (UnsupportedEncodingException e) { - servletUrlWithoutRoot = ""; - servletUrl = ""; - logger.warn("Register servlet fails", e); - return; - } - servletUrl = "/" + servletUrlWithoutRoot; - try { + servletUrl = "/" + servletUrlWithoutRoot; + httpService.registerServlet(servletUrl, this, null, httpService.createDefaultHttpContext()); - } catch (NamespaceException | ServletException e) { - logger.warn("Register servlet fails", e); + } catch (UnsupportedEncodingException | NamespaceException | ServletException e) { + throw new IllegalStateException(e.getMessage()); } } @@ -565,7 +557,7 @@ private void renderAmazonMusicPlaylistIdChannel(Connection connection, Device de } if (playLists != null) { - Map<@NonNull String, @Nullable PlayList @Nullable []> playlistMap = playLists.playlists; + Map playlistMap = playLists.playlists; if (playlistMap != null && !playlistMap.isEmpty()) { html.append(""); @@ -573,7 +565,7 @@ private void renderAmazonMusicPlaylistIdChannel(Connection connection, Device de { if (innerLists != null && innerLists.length > 0) { PlayList playList = innerLists[0]; - if (playList.playlistId != null && playList.title != null) { + if (playList != null && playList.playlistId != null && playList.title != null) { html.append("
NameValue
"); html.append(StringEscapeUtils.escapeHtml(nullReplacement(playList.title))); html.append(""); @@ -593,24 +585,32 @@ private void renderAmazonMusicPlaylistIdChannel(Connection connection, Device de private void renderBluetoothMacChannel(Connection connection, Device device, StringBuilder html) { html.append("

" + StringEscapeUtils.escapeHtml("Channel " + CHANNEL_BLUETOOTH_MAC) + "

"); JsonBluetoothStates bluetoothStates = connection.getBluetoothConnectionStates(); + if (bluetoothStates == null) { + return; + } BluetoothState[] innerStates = bluetoothStates.bluetoothStates; - if (innerStates != null) { - for (BluetoothState state : innerStates) { - if (StringUtils.equals(state.deviceSerialNumber, device.serialNumber)) { - PairedDevice[] pairedDeviceList = state.pairedDeviceList; - if (pairedDeviceList != null && pairedDeviceList.length > 0) { - html.append(""); - for (PairedDevice pairedDevice : pairedDeviceList) { - html.append(""); - } - html.append("
NameValue
"); - html.append(StringEscapeUtils.escapeHtml(nullReplacement(pairedDevice.friendlyName))); - html.append(""); - html.append(StringEscapeUtils.escapeHtml(nullReplacement(pairedDevice.address))); - html.append("
"); - } else { - html.append(StringEscapeUtils.escapeHtml("No bluetooth devices paired")); + if (innerStates == null) { + return; + } + for (BluetoothState state : innerStates) { + if (state == null) { + continue; + } + if ((state.deviceSerialNumber == null && device.serialNumber == null) + || (state.deviceSerialNumber != null && state.deviceSerialNumber.equals(device.serialNumber))) { + PairedDevice[] pairedDeviceList = state.pairedDeviceList; + if (pairedDeviceList != null && pairedDeviceList.length > 0) { + html.append(""); + for (PairedDevice pairedDevice : pairedDeviceList) { + html.append(""); } + html.append("
NameValue
"); + html.append(StringEscapeUtils.escapeHtml(nullReplacement(pairedDevice.friendlyName))); + html.append(""); + html.append(StringEscapeUtils.escapeHtml(nullReplacement(pairedDevice.address))); + html.append("
"); + } else { + html.append(StringEscapeUtils.escapeHtml("No bluetooth devices paired")); } } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlBindingConstants.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlBindingConstants.java index 9cef8ad28a221..43132e27eeb74 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlBindingConstants.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlBindingConstants.java @@ -1,112 +1,119 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.smarthome.core.thing.ThingTypeUID; -import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; - -/** - * The {@link AmazonEchoControlBindingConstants} class defines common constants, which are - * used across the whole binding. - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class AmazonEchoControlBindingConstants { - - public static final String BINDING_ID = "amazonechocontrol"; - public static final String BINDING_NAME = "Amazon Echo Control"; - - // List of all Thing Type UIDs - public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account"); - public static final ThingTypeUID THING_TYPE_ECHO = new ThingTypeUID(BINDING_ID, "echo"); - public static final ThingTypeUID THING_TYPE_ECHO_SPOT = new ThingTypeUID(BINDING_ID, "echospot"); - public static final ThingTypeUID THING_TYPE_ECHO_SHOW = new ThingTypeUID(BINDING_ID, "echoshow"); - public static final ThingTypeUID THING_TYPE_ECHO_WHA = new ThingTypeUID(BINDING_ID, "wha"); - - public static final ThingTypeUID THING_TYPE_FLASH_BRIEFING_PROFILE = new ThingTypeUID(BINDING_ID, - "flashbriefingprofile"); - - public static final Set SUPPORTED_THING_TYPES_UIDS = new HashSet<>( - Arrays.asList(THING_TYPE_ACCOUNT, THING_TYPE_ECHO, THING_TYPE_ECHO_SPOT, THING_TYPE_ECHO_SHOW, - THING_TYPE_ECHO_WHA, THING_TYPE_FLASH_BRIEFING_PROFILE)); - - // List of all Channel ids - public static final String CHANNEL_PLAYER = "player"; - public static final String CHANNEL_VOLUME = "volume"; - public static final String CHANNEL_EQUALIZER_TREBLE = "equalizerTreble"; - public static final String CHANNEL_EQUALIZER_MIDRANGE = "equalizerMidrange"; - public static final String CHANNEL_EQUALIZER_BASS = "equalizerBass"; - public static final String CHANNEL_ERROR = "error"; - public static final String CHANNEL_SHUFFLE = "shuffle"; - public static final String CHANNEL_LOOP = "loop"; - public static final String CHANNEL_IMAGE_URL = "imageUrl"; - public static final String CHANNEL_TITLE = "title"; - public static final String CHANNEL_SUBTITLE1 = "subtitle1"; - public static final String CHANNEL_SUBTITLE2 = "subtitle2"; - public static final String CHANNEL_PROVIDER_DISPLAY_NAME = "providerDisplayName"; - public static final String CHANNEL_BLUETOOTH_MAC = "bluetoothMAC"; - public static final String CHANNEL_BLUETOOTH = "bluetooth"; - public static final String CHANNEL_BLUETOOTH_DEVICE_NAME = "bluetoothDeviceName"; - public static final String CHANNEL_RADIO_STATION_ID = "radioStationId"; - public static final String CHANNEL_RADIO = "radio"; - public static final String CHANNEL_AMAZON_MUSIC_TRACK_ID = "amazonMusicTrackId"; - public static final String CHANNEL_AMAZON_MUSIC = "amazonMusic"; - public static final String CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID = "amazonMusicPlayListId"; - public static final String CHANNEL_TEXT_TO_SPEECH = "textToSpeech"; - public static final String CHANNEL_TEXT_TO_SPEECH_VOLUME = "textToSpeechVolume"; - public static final String CHANNEL_REMIND = "remind"; - public static final String CHANNEL_PLAY_ALARM_SOUND = "playAlarmSound"; - public static final String CHANNEL_START_ROUTINE = "startRoutine"; - public static final String CHANNEL_MUSIC_PROVIDER_ID = "musicProviderId"; - public static final String CHANNEL_PLAY_MUSIC_VOICE_COMMAND = "playMusicVoiceCommand"; - public static final String CHANNEL_START_COMMAND = "startCommand"; - public static final String CHANNEL_LAST_VOICE_COMMAND = "lastVoiceCommand"; - public static final String CHANNEL_MEDIA_PROGRESS = "mediaProgress"; - public static final String CHANNEL_MEDIA_LENGTH = "mediaLength"; - public static final String CHANNEL_MEDIA_PROGRESS_TIME = "mediaProgressTime"; - public static final String CHANNEL_ASCENDING_ALARM = "ascendingAlarm"; - public static final String CHANNEL_NOTIFICATION_VOLUME = "notificationVolume"; - public static final String CHANNEL_NEXT_REMINDER = "nextReminder"; - public static final String CHANNEL_NEXT_ALARM = "nextAlarm"; - public static final String CHANNEL_NEXT_MUSIC_ALARM = "nextMusicAlarm"; - public static final String CHANNEL_NEXT_TIMER = "nextTimer"; - - public static final String CHANNEL_SAVE = "save"; - public static final String CHANNEL_ACTIVE = "active"; - public static final String CHANNEL_PLAY_ON_DEVICE = "playOnDevice"; - - // List of channel Type UIDs - public static final ChannelTypeUID CHANNEL_TYPE_BLUETHOOTH_MAC = new ChannelTypeUID(BINDING_ID, "bluetoothMAC"); - public static final ChannelTypeUID CHANNEL_TYPE_AMAZON_MUSIC_PLAY_LIST_ID = new ChannelTypeUID(BINDING_ID, - "amazonMusicPlayListId"); - public static final ChannelTypeUID CHANNEL_TYPE_PLAY_ALARM_SOUND = new ChannelTypeUID(BINDING_ID, "playAlarmSound"); - public static final ChannelTypeUID CHANNEL_TYPE_CHANNEL_PLAY_ON_DEVICE = new ChannelTypeUID(BINDING_ID, - "playOnDevice"); - public static final ChannelTypeUID CHANNEL_TYPE_MUSIC_PROVIDER_ID = new ChannelTypeUID(BINDING_ID, - "musicProviderId"); - public static final ChannelTypeUID CHANNEL_TYPE_START_COMMAND = new ChannelTypeUID(BINDING_ID, "startCommand"); - - // List of all Properties - public static final String DEVICE_PROPERTY_SERIAL_NUMBER = "serialNumber"; - public static final String DEVICE_PROPERTY_FAMILY = "deviceFamily"; - public static final String DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE = "configurationJson"; - - // Other - public static final String FLASH_BRIEFING_COMMAND_PREFIX = "FlashBriefing."; -} +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; + +/** + * The {@link AmazonEchoControlBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class AmazonEchoControlBindingConstants { + public static final String BINDING_ID = "amazonechocontrol"; + public static final String BINDING_NAME = "Amazon Echo Control"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account"); + public static final ThingTypeUID THING_TYPE_ECHO = new ThingTypeUID(BINDING_ID, "echo"); + public static final ThingTypeUID THING_TYPE_ECHO_SPOT = new ThingTypeUID(BINDING_ID, "echospot"); + public static final ThingTypeUID THING_TYPE_ECHO_SHOW = new ThingTypeUID(BINDING_ID, "echoshow"); + public static final ThingTypeUID THING_TYPE_ECHO_WHA = new ThingTypeUID(BINDING_ID, "wha"); + + public static final ThingTypeUID THING_TYPE_FLASH_BRIEFING_PROFILE = new ThingTypeUID(BINDING_ID, + "flashbriefingprofile"); + + public static final ThingTypeUID THING_TYPE_SMART_HOME_DEVICE = new ThingTypeUID(BINDING_ID, "smartHomeDevice"); + public static final ThingTypeUID THING_TYPE_SMART_HOME_DEVICE_GROUP = new ThingTypeUID(BINDING_ID, + "smartHomeDeviceGroup"); + + public static final Set SUPPORTED_ECHO_THING_TYPES_UIDS = new HashSet<>( + Arrays.asList(THING_TYPE_ACCOUNT, THING_TYPE_ECHO, THING_TYPE_ECHO_SPOT, THING_TYPE_ECHO_SHOW, + THING_TYPE_ECHO_WHA, THING_TYPE_FLASH_BRIEFING_PROFILE)); + + public static final Set SUPPORTED_SMART_HOME_THING_TYPES_UIDS = new HashSet<>( + Arrays.asList(THING_TYPE_SMART_HOME_DEVICE, THING_TYPE_SMART_HOME_DEVICE_GROUP)); + + // List of all Channel ids + public static final String CHANNEL_PLAYER = "player"; + public static final String CHANNEL_VOLUME = "volume"; + public static final String CHANNEL_EQUALIZER_TREBLE = "equalizerTreble"; + public static final String CHANNEL_EQUALIZER_MIDRANGE = "equalizerMidrange"; + public static final String CHANNEL_EQUALIZER_BASS = "equalizerBass"; + public static final String CHANNEL_ERROR = "error"; + public static final String CHANNEL_SHUFFLE = "shuffle"; + public static final String CHANNEL_LOOP = "loop"; + public static final String CHANNEL_IMAGE_URL = "imageUrl"; + public static final String CHANNEL_TITLE = "title"; + public static final String CHANNEL_SUBTITLE1 = "subtitle1"; + public static final String CHANNEL_SUBTITLE2 = "subtitle2"; + public static final String CHANNEL_PROVIDER_DISPLAY_NAME = "providerDisplayName"; + public static final String CHANNEL_BLUETOOTH_MAC = "bluetoothMAC"; + public static final String CHANNEL_BLUETOOTH = "bluetooth"; + public static final String CHANNEL_BLUETOOTH_DEVICE_NAME = "bluetoothDeviceName"; + public static final String CHANNEL_RADIO_STATION_ID = "radioStationId"; + public static final String CHANNEL_RADIO = "radio"; + public static final String CHANNEL_AMAZON_MUSIC_TRACK_ID = "amazonMusicTrackId"; + public static final String CHANNEL_AMAZON_MUSIC = "amazonMusic"; + public static final String CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID = "amazonMusicPlayListId"; + public static final String CHANNEL_TEXT_TO_SPEECH = "textToSpeech"; + public static final String CHANNEL_TEXT_TO_SPEECH_VOLUME = "textToSpeechVolume"; + public static final String CHANNEL_REMIND = "remind"; + public static final String CHANNEL_PLAY_ALARM_SOUND = "playAlarmSound"; + public static final String CHANNEL_START_ROUTINE = "startRoutine"; + public static final String CHANNEL_MUSIC_PROVIDER_ID = "musicProviderId"; + public static final String CHANNEL_PLAY_MUSIC_VOICE_COMMAND = "playMusicVoiceCommand"; + public static final String CHANNEL_START_COMMAND = "startCommand"; + public static final String CHANNEL_LAST_VOICE_COMMAND = "lastVoiceCommand"; + public static final String CHANNEL_MEDIA_PROGRESS = "mediaProgress"; + public static final String CHANNEL_MEDIA_LENGTH = "mediaLength"; + public static final String CHANNEL_MEDIA_PROGRESS_TIME = "mediaProgressTime"; + public static final String CHANNEL_ASCENDING_ALARM = "ascendingAlarm"; + public static final String CHANNEL_NOTIFICATION_VOLUME = "notificationVolume"; + public static final String CHANNEL_NEXT_REMINDER = "nextReminder"; + public static final String CHANNEL_NEXT_ALARM = "nextAlarm"; + public static final String CHANNEL_NEXT_MUSIC_ALARM = "nextMusicAlarm"; + public static final String CHANNEL_NEXT_TIMER = "nextTimer"; + + public static final String CHANNEL_SAVE = "save"; + public static final String CHANNEL_ACTIVE = "active"; + public static final String CHANNEL_PLAY_ON_DEVICE = "playOnDevice"; + + // List of channel Type UIDs + public static final ChannelTypeUID CHANNEL_TYPE_BLUETHOOTH_MAC = new ChannelTypeUID(BINDING_ID, "bluetoothMAC"); + public static final ChannelTypeUID CHANNEL_TYPE_AMAZON_MUSIC_PLAY_LIST_ID = new ChannelTypeUID(BINDING_ID, + "amazonMusicPlayListId"); + public static final ChannelTypeUID CHANNEL_TYPE_PLAY_ALARM_SOUND = new ChannelTypeUID(BINDING_ID, "playAlarmSound"); + public static final ChannelTypeUID CHANNEL_TYPE_CHANNEL_PLAY_ON_DEVICE = new ChannelTypeUID(BINDING_ID, + "playOnDevice"); + public static final ChannelTypeUID CHANNEL_TYPE_MUSIC_PROVIDER_ID = new ChannelTypeUID(BINDING_ID, + "musicProviderId"); + public static final ChannelTypeUID CHANNEL_TYPE_START_COMMAND = new ChannelTypeUID(BINDING_ID, "startCommand"); + + // List of all Properties + public static final String DEVICE_PROPERTY_SERIAL_NUMBER = "serialNumber"; + public static final String DEVICE_PROPERTY_FAMILY = "deviceFamily"; + public static final String DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE = "configurationJson"; + public static final String DEVICE_PROPERTY_ID = "id"; + + // Other + public static final String FLASH_BRIEFING_COMMAND_PREFIX = "FlashBriefing."; +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 69bb3d816e778..91d3ae019a8c0 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -1,177 +1,157 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal; - -import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*; - -import java.util.HashMap; -import java.util.Hashtable; -import java.util.Map; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.smarthome.config.discovery.DiscoveryService; -import org.eclipse.smarthome.core.storage.Storage; -import org.eclipse.smarthome.core.storage.StorageService; -import org.eclipse.smarthome.core.thing.Bridge; -import org.eclipse.smarthome.core.thing.Thing; -import org.eclipse.smarthome.core.thing.ThingTypeUID; -import org.eclipse.smarthome.core.thing.ThingUID; -import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; -import org.eclipse.smarthome.core.thing.binding.ThingHandler; -import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; -import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; -import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler; -import org.openhab.binding.amazonechocontrol.internal.handler.EchoHandler; -import org.openhab.binding.amazonechocontrol.internal.handler.FlashBriefingProfileHandler; -import org.osgi.framework.ServiceRegistration; -import org.osgi.service.component.ComponentContext; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Reference; -import org.osgi.service.component.annotations.ReferenceCardinality; -import org.osgi.service.component.annotations.ReferencePolicy; -import org.osgi.service.http.HttpService; - -import com.google.gson.Gson; - -/** - * The {@link AmazonEchoControlHandlerFactory} is responsible for creating things and thing - * handlers. - * - * @author Michael Geramb - Initial contribution - */ -@Component(service = ThingHandlerFactory.class, configurationPid = "binding.amazonechocontrol") -@NonNullByDefault -public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory { - - private final Map> discoveryServiceRegistrations = new HashMap<>(); - - @Nullable - HttpService httpService; - @Nullable - StorageService storageService; - @Nullable - BindingServlet bindingServlet; - @Nullable - Gson gson; - - @Override - public boolean supportsThingType(ThingTypeUID thingTypeUID) { - return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); - } - - @Override - protected void activate(ComponentContext componentContext) { - super.activate(componentContext); - HttpService httpService = this.httpService; - if (bindingServlet == null && httpService != null) { - bindingServlet = new BindingServlet(httpService); - } - } - - @Override - protected void deactivate(ComponentContext componentContext) { - BindingServlet bindingServlet = this.bindingServlet; - this.bindingServlet = null; - if (bindingServlet != null) { - bindingServlet.dispose(); - } - super.deactivate(componentContext); - } - - @Override - protected @Nullable ThingHandler createHandler(Thing thing) { - ThingTypeUID thingTypeUID = thing.getThingTypeUID(); - - HttpService httpService = this.httpService; - if (httpService == null) { - return null; - } - StorageService storageService = this.storageService; - if (storageService == null) { - return null; - } - Gson gson = this.gson; - if (gson == null) { - gson = new Gson(); - this.gson = gson; - } - - if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) { - Storage storage = storageService.getStorage(thing.getUID().toString(), - String.class.getClassLoader()); - AccountHandler bridgeHandler = new AccountHandler((Bridge) thing, httpService, storage, gson); - registerDiscoveryService(bridgeHandler); - BindingServlet bindingServlet = this.bindingServlet; - if (bindingServlet != null) { - bindingServlet.addAccountThing(thing); - } - return bridgeHandler; - } - if (thingTypeUID.equals(THING_TYPE_FLASH_BRIEFING_PROFILE)) { - Storage storage = storageService.getStorage(thing.getUID().toString(), - String.class.getClassLoader()); - return new FlashBriefingProfileHandler(thing, storage); - } - if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { - return new EchoHandler(thing, gson); - } - return null; - } - - private synchronized void registerDiscoveryService(AccountHandler bridgeHandler) { - AmazonEchoDiscovery discoveryService = new AmazonEchoDiscovery(bridgeHandler); - discoveryService.activate(); - this.discoveryServiceRegistrations.put(bridgeHandler.getThing().getUID(), - bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>())); - } - - @Override - protected synchronized void removeHandler(ThingHandler thingHandler) { - if (thingHandler instanceof AccountHandler) { - BindingServlet bindingServlet = this.bindingServlet; - if (bindingServlet != null) { - bindingServlet.removeAccountThing(thingHandler.getThing()); - } - - ServiceRegistration serviceReg = this.discoveryServiceRegistrations - .remove(thingHandler.getThing().getUID()); - if (serviceReg != null) { - // remove discovery service, if bridge handler is removed - AmazonEchoDiscovery service = (AmazonEchoDiscovery) bundleContext.getService(serviceReg.getReference()); - serviceReg.unregister(); - if (service != null) { - service.deactivate(); - } - } - } - } - - @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC) - protected void setHttpService(HttpService httpService) { - this.httpService = httpService; - } - - protected void unsetHttpService(HttpService httpService) { - this.httpService = null; - } - - @Reference - protected void setStorageService(StorageService storageService) { - this.storageService = storageService; - } - - protected void unsetStorageService(StorageService storageService) { - this.storageService = null; - } -} +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal; + +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.SUPPORTED_ECHO_THING_TYPES_UIDS; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.SUPPORTED_SMART_HOME_THING_TYPES_UIDS; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.THING_TYPE_ACCOUNT; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.THING_TYPE_FLASH_BRIEFING_PROFILE; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; +import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.storage.Storage; +import org.eclipse.smarthome.core.storage.StorageService; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; +import org.openhab.binding.amazonechocontrol.internal.discovery.SmartHomeDevicesDiscovery; +import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler; +import org.openhab.binding.amazonechocontrol.internal.handler.EchoHandler; +import org.openhab.binding.amazonechocontrol.internal.handler.FlashBriefingProfileHandler; +import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.http.HttpService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * The {@link AmazonEchoControlHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Michael Geramb - Initial contribution + */ +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.amazonechocontrol") +@NonNullByDefault +public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory { + private final Logger logger = LoggerFactory.getLogger(AmazonEchoControlHandlerFactory.class); + private final Map>> discoveryServiceRegistrations = new HashMap<>(); + + private final HttpService httpService; + private final StorageService storageService; + private final BindingServlet bindingServlet; + private final Gson gson; + + @Activate + public AmazonEchoControlHandlerFactory(@Reference HttpService httpService, + @Reference StorageService storageService) { + this.storageService = storageService; + this.httpService = httpService; + this.gson = new Gson(); + this.bindingServlet = new BindingServlet(httpService); + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_ECHO_THING_TYPES_UIDS.contains(thingTypeUID) + || SUPPORTED_SMART_HOME_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected void deactivate(ComponentContext componentContext) { + bindingServlet.dispose(); + super.deactivate(componentContext); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) { + Storage storage = storageService.getStorage(thing.getUID().toString(), + String.class.getClassLoader()); + AccountHandler bridgeHandler = new AccountHandler((Bridge) thing, httpService, storage, gson); + registerDiscoveryService(bridgeHandler); + bindingServlet.addAccountThing(thing); + return bridgeHandler; + } else if (thingTypeUID.equals(THING_TYPE_FLASH_BRIEFING_PROFILE)) { + Storage storage = storageService.getStorage(thing.getUID().toString(), + String.class.getClassLoader()); + return new FlashBriefingProfileHandler(thing, storage); + } else if (SUPPORTED_ECHO_THING_TYPES_UIDS.contains(thingTypeUID)) { + return new EchoHandler(thing, gson); + } else if (SUPPORTED_SMART_HOME_THING_TYPES_UIDS.contains(thingTypeUID)) { + return new SmartHomeDeviceHandler(thing, gson); + } + return null; + } + + private synchronized void registerDiscoveryService(AccountHandler bridgeHandler) { + List> discoveryServiceRegistration = discoveryServiceRegistrations + .computeIfAbsent(bridgeHandler.getThing().getUID(), k -> new ArrayList<>()); + SmartHomeDevicesDiscovery smartHomeDevicesDiscovery = new SmartHomeDevicesDiscovery(bridgeHandler); + smartHomeDevicesDiscovery.activate(); + discoveryServiceRegistration.add(bundleContext.registerService(DiscoveryService.class.getName(), + smartHomeDevicesDiscovery, new Hashtable<>())); + + AmazonEchoDiscovery discoveryService = new AmazonEchoDiscovery(bridgeHandler); + discoveryService.activate(); + discoveryServiceRegistration.add( + bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>())); + } + + @Override + protected synchronized void removeHandler(ThingHandler thingHandler) { + if (thingHandler instanceof AccountHandler) { + BindingServlet bindingServlet = this.bindingServlet; + bindingServlet.removeAccountThing(thingHandler.getThing()); + + List> discoveryServiceRegistration = discoveryServiceRegistrations + .remove(thingHandler.getThing().getUID()); + if (discoveryServiceRegistration != null) { + discoveryServiceRegistration.forEach(serviceReg -> { + AbstractDiscoveryService service = (AbstractDiscoveryService) bundleContext + .getService(serviceReg.getReference()); + serviceReg.unregister(); + if (service != null) { + if (service instanceof AmazonEchoDiscovery) { + ((AmazonEchoDiscovery) service).deactivate(); + } else if (service instanceof SmartHomeDevicesDiscovery) { + ((SmartHomeDevicesDiscovery) service).deactivate(); + } else { + logger.warn("Found unknown discovery-service instance: {}", service); + } + } + }); + } + } + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoDynamicStateDescriptionProvider.java similarity index 85% rename from bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java rename to bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoDynamicStateDescriptionProvider.java index 93cc6c5a27af5..807439a1cf6ac 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoDynamicStateDescriptionProvider.java @@ -10,9 +10,16 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.amazonechocontrol.internal.statedescription; +package org.openhab.binding.amazonechocontrol.internal; -import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.BINDING_ID; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.CHANNEL_TYPE_AMAZON_MUSIC_PLAY_LIST_ID; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.CHANNEL_TYPE_BLUETHOOTH_MAC; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.CHANNEL_TYPE_CHANNEL_PLAY_ON_DEVICE; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.CHANNEL_TYPE_MUSIC_PROVIDER_ID; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.CHANNEL_TYPE_PLAY_ALARM_SOUND; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.CHANNEL_TYPE_START_COMMAND; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.FLASH_BRIEFING_COMMAND_PREFIX; import java.util.ArrayList; import java.util.Arrays; @@ -28,11 +35,11 @@ import org.eclipse.smarthome.core.thing.ThingRegistry; import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; import org.eclipse.smarthome.core.thing.type.DynamicStateDescriptionProvider; import org.eclipse.smarthome.core.types.StateDescription; import org.eclipse.smarthome.core.types.StateDescriptionFragmentBuilder; import org.eclipse.smarthome.core.types.StateOption; -import org.openhab.binding.amazonechocontrol.internal.Connection; import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler; import org.openhab.binding.amazonechocontrol.internal.handler.EchoHandler; import org.openhab.binding.amazonechocontrol.internal.handler.FlashBriefingProfileHandler; @@ -43,10 +50,9 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists.PlayList; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; -import org.osgi.service.component.annotations.ReferenceCardinality; -import org.osgi.service.component.annotations.ReferencePolicy; /** * Dynamic channel state description provider. @@ -57,23 +63,14 @@ @Component(service = { DynamicStateDescriptionProvider.class, AmazonEchoDynamicStateDescriptionProvider.class }) @NonNullByDefault public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDescriptionProvider { + private final ThingRegistry thingRegistry; - private @Nullable ThingRegistry thingRegistry; - - @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC) - protected void setThingRegistry(ThingRegistry thingRegistry) { - this.thingRegistry = thingRegistry; - } - - protected void unsetThingRegistry(ThingRegistry thingRegistry) { + @Activate + public AmazonEchoDynamicStateDescriptionProvider(@Reference ThingRegistry thingRegistry) { this.thingRegistry = thingRegistry; } public @Nullable ThingHandler findHandler(Channel channel) { - ThingRegistry thingRegistry = this.thingRegistry; - if (thingRegistry == null) { - return null; - } Thing thing = thingRegistry.get(channel.getUID().getThingUID()); if (thing == null) { return null; @@ -100,13 +97,14 @@ protected void unsetThingRegistry(ThingRegistry thingRegistry) { @Override public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { - if (originalStateDescription == null) { + ChannelTypeUID channelTypeUID = channel.getChannelTypeUID(); + if (channelTypeUID == null || !BINDING_ID.equals(channelTypeUID.getBindingId())) { return null; } - ThingRegistry thingRegistry = this.thingRegistry; - if (thingRegistry == null) { + if (originalStateDescription == null) { return null; } + if (CHANNEL_TYPE_BLUETHOOTH_MAC.equals(channel.getChannelTypeUID())) { EchoHandler handler = (EchoHandler) findHandler(channel); if (handler == null) { @@ -135,7 +133,6 @@ protected void unsetThingRegistry(ThingRegistry thingRegistry) { StateDescription result = StateDescriptionFragmentBuilder.create(originalStateDescription) .withOptions(options).build().toStateDescription(); return result; - } else if (CHANNEL_TYPE_AMAZON_MUSIC_PLAY_LIST_ID.equals(channel.getChannelTypeUID())) { EchoHandler handler = (EchoHandler) findHandler(channel); if (handler == null) { @@ -214,9 +211,8 @@ protected void unsetThingRegistry(ThingRegistry thingRegistry) { options.add(new StateOption(value, device.accountName)); } } - StateDescription result = StateDescriptionFragmentBuilder.create(originalStateDescription) - .withOptions(options).build().toStateDescription(); - return result; + return StateDescriptionFragmentBuilder.create(originalStateDescription).withOptions(options).build() + .toStateDescription(); } else if (CHANNEL_TYPE_MUSIC_PROVIDER_ID.equals(channel.getChannelTypeUID())) { EchoHandler handler = (EchoHandler) findHandler(channel); if (handler == null) { @@ -240,9 +236,8 @@ protected void unsetThingRegistry(ThingRegistry thingRegistry) { options.add(new StateOption(providerId, displayName)); } } - StateDescription result = StateDescriptionFragmentBuilder.create(originalStateDescription) - .withOptions(options).build().toStateDescription(); - return result; + return StateDescriptionFragmentBuilder.create(originalStateDescription).withOptions(options).build() + .toStateDescription(); } else if (CHANNEL_TYPE_START_COMMAND.equals(channel.getChannelTypeUID())) { EchoHandler handler = (EchoHandler) findHandler(channel); if (handler == null) { @@ -265,9 +260,8 @@ protected void unsetThingRegistry(ThingRegistry thingRegistry) { String displayName = flashBriefing.getThing().getLabel(); options.add(new StateOption(value, displayName)); } - StateDescription result = StateDescriptionFragmentBuilder.create(originalStateDescription) - .withOptions(options).build().toStateDescription(); - return result; + return StateDescriptionFragmentBuilder.create(originalStateDescription).withOptions(options).build() + .toStateDescription(); } return null; } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 51c1ebfed0371..237c339c0249b 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -29,6 +29,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; @@ -36,6 +37,7 @@ import java.util.Map; import java.util.Random; import java.util.Scanner; +import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; @@ -73,6 +75,7 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNetworkDetails; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationRequest; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound; @@ -91,37 +94,39 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse.Success; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse.Tokens; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonRenewTokenResponse; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroups.SmartHomeGroup; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonStartRoutineRequest; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonUsersMeResponse; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonWakeWords; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonWakeWords.WakeWord; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonWebSiteCookie; +import org.openhab.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonSyntaxException; /** - * The {@link Connection} is responsible for the connection to the amazon server and - * handling of the commands + * The {@link Connection} is responsible for the connection to the amazon server + * and handling of the commands * * @author Michael Geramb - Initial contribution */ @NonNullByDefault public class Connection { - private static final String THING_THREADPOOL_NAME = "thingHandler"; + private static final long EXPIRES_IN = 432000; // five days + private static final Pattern CHARSET_PATTERN = Pattern.compile("(?i)\\bcharset=\\s*\"?([^\\s;\"]*)"); protected final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(THING_THREADPOOL_NAME); - private static final long expiresIn = 432000; // five days - private static final Pattern charsetPattern = Pattern.compile("(?i)\\bcharset=\\s*\"?([^\\s;\"]*)"); - private final Logger logger = LoggerFactory.getLogger(Connection.class); private final CookieManager cookieManager = new CookieManager(); @@ -152,7 +157,6 @@ public Connection(@Nullable Connection oldConnection, Gson gson) { frc = oldConnection.getFrc(); serial = oldConnection.getSerial(); deviceId = oldConnection.getDeviceId(); - } Random rand = new Random(); if (frc != null) { @@ -440,13 +444,15 @@ public boolean tryRestoreLogin(@Nullable String data, @Nullable String overloade try { String bootstrapResultJson = convertStream(connection); JsonBootstrapResult result = parseJson(bootstrapResultJson, JsonBootstrapResult.class); - Authentication authentication = result.authentication; - if (authentication != null && authentication.authenticated) { - this.customerName = authentication.customerName; - if (this.accountCustomerId == null) { - this.accountCustomerId = authentication.customerId; + if (result != null) { + Authentication authentication = result.authentication; + if (authentication != null && authentication.authenticated) { + this.customerName = authentication.customerName; + if (this.accountCustomerId == null) { + this.accountCustomerId = authentication.customerId; + } + return authentication; } - return authentication; } } catch (JsonSyntaxException | IllegalStateException e) { logger.info("No valid json received", e); @@ -471,7 +477,7 @@ public String convertStream(HttpsURLConnection connection) throws IOException { String contentType = connection.getContentType(); String charSet = null; if (contentType != null) { - Matcher m = charsetPattern.matcher(contentType); + Matcher m = CHARSET_PATTERN.matcher(contentType); if (m.find()) { charSet = m.group(1).trim().toUpperCase(); } @@ -498,7 +504,7 @@ public String makeRequestAndReturnString(String verb, String url, @Nullable Stri @Nullable Map customHeaders) throws IOException, URISyntaxException { HttpsURLConnection connection = makeRequest(verb, url, postData, json, true, customHeaders, 0); String result = convertStream(connection); - this.logger.debug("Result of {} {}:{}", verb, url, result); + logger.debug("Result of {} {}:{}", verb, url, result); return result; } @@ -507,10 +513,9 @@ public HttpsURLConnection makeRequest(String verb, String url, @Nullable String throws IOException, URISyntaxException { String currentUrl = url; int redirectCounter = 0; - while (true) // loop for handling redirect and bad request, using automatic redirect is not possible, - // because - // all response headers must be catched - { + // loop for handling redirect and bad request, using automatic redirect is not + // possible, because all response headers must be catched + while (true) { int code; HttpsURLConnection connection = null; try { @@ -693,6 +698,9 @@ public String registerConnectionAsApp(String oAutRedirectUrl) registerAppRequestJson, true, registerHeaders); JsonRegisterAppResponse registerAppResponse = parseJson(registerAppResultJson, JsonRegisterAppResponse.class); + if (registerAppResponse == null) { + throw new ConnectionException("Error: No response receivec from register application"); + } Response response = registerAppResponse.response; if (response == null) { throw new ConnectionException("Error: No response received from register application"); @@ -701,7 +709,6 @@ public String registerConnectionAsApp(String oAutRedirectUrl) if (success == null) { throw new ConnectionException("Error: No success received from register application"); } - Tokens tokens = success.tokens; if (tokens == null) { throw new ConnectionException("Error: No tokens received from register application"); @@ -710,7 +717,7 @@ public String registerConnectionAsApp(String oAutRedirectUrl) if (bearer == null) { throw new ConnectionException("Error: No bearer received from register application"); } - this.refreshToken = bearer.refresh_token; + this.refreshToken = bearer.refreshToken; if (StringUtils.isEmpty(this.refreshToken)) { throw new ConnectionException("Error: No refresh token received"); } @@ -720,6 +727,9 @@ public String registerConnectionAsApp(String oAutRedirectUrl) String usersMeResponseJson = makeRequestAndReturnString("GET", "https://alexa.amazon.com/api/users/me?platform=ios&version=2.2.223830.0", null, false, null); JsonUsersMeResponse usersMeResponse = parseJson(usersMeResponseJson, JsonUsersMeResponse.class); + if (usersMeResponse == null) { + throw new IllegalArgumentException("Received no response on me-request"); + } URI uri = new URI(usersMeResponse.marketPlaceDomainName); String host = uri.getHost(); @@ -734,9 +744,9 @@ public String registerConnectionAsApp(String oAutRedirectUrl) String deviceName = null; Extensions extensions = success.extensions; if (extensions != null) { - DeviceInfo deviceInfo = extensions.device_info; + DeviceInfo deviceInfo = extensions.deviceInfo; if (deviceInfo != null) { - deviceName = deviceInfo.device_name; + deviceName = deviceInfo.deviceName; } } if (deviceName == null) { @@ -775,10 +785,10 @@ private void exchangeToken() throws IOException, URISyntaxException { Cookie[] cookies = cookiesMap.get(domain); for (Cookie cookie : cookies) { if (cookie != null) { - HttpCookie httpCookie = new HttpCookie(cookie.Name, cookie.Value); - httpCookie.setPath(cookie.Path); + HttpCookie httpCookie = new HttpCookie(cookie.name, cookie.value); + httpCookie.setPath(cookie.path); httpCookie.setDomain(domain); - Boolean secure = cookie.Secure; + Boolean secure = cookie.secure; if (secure != null) { httpCookie.setSecure(secure); } @@ -792,7 +802,7 @@ private void exchangeToken() throws IOException, URISyntaxException { if (!verifyLogin()) { throw new ConnectionException("Verify login failed after token exchange"); } - this.renewTime = (long) (System.currentTimeMillis() + Connection.expiresIn * 1000d / 0.8d); // start renew at + this.renewTime = (long) (System.currentTimeMillis() + Connection.EXPIRES_IN * 1000d / 0.8d); // start renew at } public boolean checkRenewSession() throws UnknownHostException, URISyntaxException, IOException { @@ -876,7 +886,7 @@ public void logout() { } // parser - private T parseJson(String json, Class type) throws JsonSyntaxException, IllegalStateException { + private @Nullable T parseJson(String json, Class type) throws JsonSyntaxException, IllegalStateException { try { return gson.fromJson(json, type); } catch (JsonParseException | IllegalStateException e) { @@ -893,9 +903,11 @@ public WakeWord[] getWakeWords() { try { json = makeRequestAndReturnString(alexaServer + "/api/wake-word?cached=true"); JsonWakeWords wakeWords = parseJson(json, JsonWakeWords.class); - WakeWord[] result = wakeWords.wakeWords; - if (result != null) { - return result; + if (wakeWords != null) { + WakeWord[] result = wakeWords.wakeWords; + if (result != null) { + return result; + } } } catch (IOException | URISyntaxException e) { logger.info("getting wakewords failed", e); @@ -903,14 +915,59 @@ public WakeWord[] getWakeWords() { return new WakeWord[0]; } + public List getSmarthomeDeviceList() throws IOException, URISyntaxException { + try { + String json = makeRequestAndReturnString(alexaServer + "/api/phoenix"); + logger.debug("getSmartHomeDevices result: {}", json); + + JsonNetworkDetails networkDetails = parseJson(json, JsonNetworkDetails.class); + if (networkDetails == null) { + throw new IllegalArgumentException("received no response on network detail request"); + } + Object jsonObject = gson.fromJson(networkDetails.networkDetail, Object.class); + List result = new ArrayList<>(); + searchSmartHomeDevicesRecursive(jsonObject, result); + + return result; + } catch (Exception e) { + logger.warn("getSmartHomeDevices fails: {}", e.getMessage()); + throw e; + } + } + + private void searchSmartHomeDevicesRecursive(@Nullable Object jsonNode, List devices) { + if (jsonNode instanceof Map) { + @SuppressWarnings("rawtypes") + Map map = (Map) jsonNode; + if (map.containsKey("entityId") && map.containsKey("friendlyName") && map.containsKey("actions")) { + // device node found, create type element and add it to the results + JsonElement element = gson.toJsonTree(jsonNode); + SmartHomeDevice shd = parseJson(element.toString(), SmartHomeDevice.class); + if (shd != null) { + devices.add(shd); + } + } else if (map.containsKey("applianceGroupName")) { + JsonElement element = gson.toJsonTree(jsonNode); + SmartHomeGroup shg = parseJson(element.toString(), SmartHomeGroup.class); + if (shg != null) { + devices.add(shg); + } + } else { + map.values().forEach(value -> searchSmartHomeDevicesRecursive(value, devices)); + } + } + } + public List getDeviceList() throws IOException, URISyntaxException { String json = getDeviceListJson(); JsonDevices devices = parseJson(json, JsonDevices.class); - Device[] result = devices.devices; - if (result == null) { - return new ArrayList<>(); + if (devices != null) { + Device[] result = devices.devices; + if (result != null) { + return new ArrayList<>(Arrays.asList(result)); + } } - return new ArrayList<>(Arrays.asList(result)); + return Collections.emptyList(); } public String getDeviceListJson() throws IOException, URISyntaxException { @@ -918,14 +975,44 @@ public String getDeviceListJson() throws IOException, URISyntaxException { return json; } - public JsonPlayerState getPlayer(Device device) throws IOException, URISyntaxException { + public Map getSmartHomeDeviceStatesJson(Set applianceIds) + throws IOException, URISyntaxException { + JsonObject requestObject = new JsonObject(); + JsonArray stateRequests = new JsonArray(); + for (String applianceId : applianceIds) { + JsonObject stateRequest = new JsonObject(); + stateRequest.addProperty("entityId", applianceId); + stateRequest.addProperty("entityType", "APPLIANCE"); + stateRequests.add(stateRequest); + } + requestObject.add("stateRequests", stateRequests); + String requestBody = requestObject.toString(); + String json = makeRequestAndReturnString("POST", alexaServer + "/api/phoenix/state", requestBody, true, null); + logger.trace("Requested {} and received {}", requestBody, json); + + JsonObject responseObject = this.gson.fromJson(json, JsonObject.class); + JsonArray deviceStates = (JsonArray) responseObject.get("deviceStates"); + Map result = new HashMap<>(); + for (JsonElement deviceState : deviceStates) { + JsonObject deviceStateObject = deviceState.getAsJsonObject(); + JsonObject entity = deviceStateObject.get("entity").getAsJsonObject(); + String applicanceId = entity.get("entityId").getAsString(); + JsonElement capabilityState = deviceStateObject.get("capabilityStates"); + if (capabilityState != null && capabilityState.isJsonArray()) { + result.put(applicanceId, capabilityState.getAsJsonArray()); + } + } + return result; + } + + public @Nullable JsonPlayerState getPlayer(Device device) throws IOException, URISyntaxException { String json = makeRequestAndReturnString(alexaServer + "/api/np/player?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&screenWidth=1440"); JsonPlayerState playerState = parseJson(json, JsonPlayerState.class); return playerState; } - public JsonMediaState getMediaState(Device device) throws IOException, URISyntaxException { + public @Nullable JsonMediaState getMediaState(Device device) throws IOException, URISyntaxException { String json = makeRequestAndReturnString(alexaServer + "/api/media/state?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType); JsonMediaState mediaState = parseJson(json, JsonMediaState.class); @@ -938,9 +1025,11 @@ public Activity[] getActivities(int number, @Nullable Long startTime) { json = makeRequestAndReturnString(alexaServer + "/api/activities?startTime=" + (startTime != null ? startTime : "") + "&size=" + number + "&offset=1"); JsonActivities activities = parseJson(json, JsonActivities.class); - Activity[] activiesArray = activities.activities; - if (activiesArray != null) { - return activiesArray; + if (activities != null) { + Activity[] activiesArray = activities.activities; + if (activiesArray != null) { + return activiesArray; + } } } catch (IOException | URISyntaxException e) { logger.info("getting activities failed", e); @@ -948,7 +1037,7 @@ public Activity[] getActivities(int number, @Nullable Long startTime) { return new Activity[0]; } - public JsonBluetoothStates getBluetoothConnectionStates() { + public @Nullable JsonBluetoothStates getBluetoothConnectionStates() { String json; try { json = makeRequestAndReturnString(alexaServer + "/api/bluetooth?cached=true"); @@ -960,7 +1049,7 @@ public JsonBluetoothStates getBluetoothConnectionStates() { return bluetoothStates; } - public JsonPlaylists getPlaylists(Device device) throws IOException, URISyntaxException { + public @Nullable JsonPlaylists getPlaylists(Device device) throws IOException, URISyntaxException { String json = makeRequestAndReturnString(alexaServer + "/api/cloudplayer/playlists?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + (StringUtils.isEmpty(this.accountCustomerId) ? device.deviceOwnerCustomerId @@ -975,6 +1064,63 @@ public void command(Device device, String command) throws IOException, URISyntax makeRequest("POST", url, command, true, true, null, 0); } + public void smartHomeCommand(String entityId, String action) throws IOException { + smartHomeCommand(entityId, action, null, null); + } + + public void smartHomeCommand(String entityId, String action, @Nullable String property, @Nullable Object value) + throws IOException { + String url = alexaServer + "/api/phoenix/state"; + + JsonObject json = new JsonObject(); + JsonArray controlRequests = new JsonArray(); + JsonObject controlRequest = new JsonObject(); + controlRequest.addProperty("entityId", entityId); + controlRequest.addProperty("entityType", "APPLIANCE"); + JsonObject parameters = new JsonObject(); + parameters.addProperty("action", action); + if (property != null) { + if (value instanceof Boolean) { + parameters.addProperty(property, (boolean) value); + } else if (value instanceof String) { + parameters.addProperty(property, (String) value); + } else if (value instanceof Number) { + parameters.addProperty(property, (Number) value); + } else if (value instanceof Character) { + parameters.addProperty(property, (Character) value); + } else if (value instanceof JsonElement) { + parameters.add(property, (JsonElement) value); + } + } + controlRequest.add("parameters", parameters); + controlRequests.add(controlRequest); + json.add("controlRequests", controlRequests); + + String requestBody = json.toString(); + try { + String resultBody = makeRequestAndReturnString("PUT", url, requestBody, true, null); + logger.debug("{}", resultBody); + JsonObject result = parseJson(resultBody, JsonObject.class); + if (result != null) { + JsonElement errors = result.get("errors"); + if (errors != null && errors.isJsonArray()) { + JsonArray errorList = errors.getAsJsonArray(); + if (errorList.size() > 0) { + logger.info("Smart home device command failed."); + logger.info("Request:"); + logger.info("{}", requestBody); + logger.info("Answer:"); + for (JsonElement error : errorList) { + logger.info("{}", error.toString()); + } + } + } + } + } catch (URISyntaxException e) { + logger.info("Wrong url {}", url, e); + } + } + public void notificationVolume(Device device, int volume) throws IOException, URISyntaxException { String url = alexaServer + "/api/device-notification-state/" + device.deviceType + "/" + device.softwareVersion + "/" + device.serialNumber; @@ -996,9 +1142,11 @@ public DeviceNotificationState[] getDeviceNotificationStates() { try { json = makeRequestAndReturnString(alexaServer + "/api/device-notification-state"); JsonDeviceNotificationState result = parseJson(json, JsonDeviceNotificationState.class); - DeviceNotificationState[] deviceNotificationStates = result.deviceNotificationStates; - if (deviceNotificationStates != null) { - return deviceNotificationStates; + if (result != null) { + DeviceNotificationState[] deviceNotificationStates = result.deviceNotificationStates; + if (deviceNotificationStates != null) { + return deviceNotificationStates; + } } } catch (IOException | URISyntaxException e) { logger.info("Error getting device notification states", e); @@ -1011,9 +1159,11 @@ public AscendingAlarmModel[] getAscendingAlarm() { try { json = makeRequestAndReturnString(alexaServer + "/api/ascending-alarm"); JsonAscendingAlarm result = parseJson(json, JsonAscendingAlarm.class); - AscendingAlarmModel[] ascendingAlarmModelList = result.ascendingAlarmModelList; - if (ascendingAlarmModelList != null) { - return ascendingAlarmModelList; + if (result != null) { + AscendingAlarmModel[] ascendingAlarmModelList = result.ascendingAlarmModelList; + if (ascendingAlarmModelList != null) { + return ascendingAlarmModelList; + } } } catch (IOException | URISyntaxException e) { logger.info("Error getting device notification states", e); @@ -1163,7 +1313,8 @@ private void executeSequenceCommandWithVolume(@Nullable Device device, String co } } - // commands: Alexa.Weather.Play, Alexa.Traffic.Play, Alexa.FlashBriefing.Play, Alexa.GoodMorning.Play, + // commands: Alexa.Weather.Play, Alexa.Traffic.Play, Alexa.FlashBriefing.Play, + // Alexa.GoodMorning.Play, // Alexa.SingASong.Play, Alexa.TellStory.Play, Alexa.Speak (textToSpeach) public void executeSequenceCommand(@Nullable Device device, String command, @Nullable Map parameters) throws IOException, URISyntaxException { @@ -1233,21 +1384,27 @@ private JsonObject createExecutionNode(@Nullable Device device, String command, public void startRoutine(Device device, String utterance) throws IOException, URISyntaxException { JsonAutomation found = null; String deviceLocale = ""; + JsonAutomation[] routines = getRoutines(); + if (routines == null) { + return; + } for (JsonAutomation routine : getRoutines()) { - Trigger[] triggers = routine.triggers; - if (triggers != null && routine.sequence != null) { - for (JsonAutomation.Trigger trigger : triggers) { - if (trigger == null) { - continue; - } - Payload payload = trigger.payload; - if (payload == null) { - continue; - } - if (StringUtils.equalsIgnoreCase(payload.utterance, utterance)) { - found = routine; - deviceLocale = payload.locale; - break; + if (routine != null) { + Trigger[] triggers = routine.triggers; + if (triggers != null && routine.sequence != null) { + for (JsonAutomation.Trigger trigger : triggers) { + if (trigger == null) { + continue; + } + Payload payload = trigger.payload; + if (payload == null) { + continue; + } + if (StringUtils.equalsIgnoreCase(payload.utterance, utterance)) { + found = routine; + deviceLocale = payload.locale; + break; + } } } } @@ -1297,7 +1454,7 @@ public void startRoutine(Device device, String utterance) throws IOException, UR } } - public JsonAutomation[] getRoutines() throws IOException, URISyntaxException { + public @Nullable JsonAutomation @Nullable [] getRoutines() throws IOException, URISyntaxException { String json = makeRequestAndReturnString(alexaServer + "/api/behaviors/automations?limit=2000"); JsonAutomation[] result = parseJson(json, JsonAutomation[].class); return result; @@ -1306,6 +1463,9 @@ public JsonAutomation[] getRoutines() throws IOException, URISyntaxException { public JsonFeed[] getEnabledFlashBriefings() throws IOException, URISyntaxException { String json = makeRequestAndReturnString(alexaServer + "/api/content-skills/enabled-feeds"); JsonEnabledFeeds result = parseJson(json, JsonEnabledFeeds.class); + if (result == null) { + return new JsonFeed[0]; + } JsonFeed[] enabledFeeds = result.enabledFeeds; if (enabledFeeds != null) { return enabledFeeds; @@ -1325,6 +1485,9 @@ public JsonNotificationSound[] getNotificationSounds(Device device) throws IOExc alexaServer + "/api/notification/sounds?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&softwareVersion=" + device.softwareVersion); JsonNotificationSounds result = parseJson(json, JsonNotificationSounds.class); + if (result == null) { + return new JsonNotificationSound[0]; + } JsonNotificationSound[] notificationSounds = result.notificationSounds; if (notificationSounds != null) { return notificationSounds; @@ -1335,6 +1498,9 @@ public JsonNotificationSound[] getNotificationSounds(Device device) throws IOExc public JsonNotificationResponse[] notifications() throws IOException, URISyntaxException { String response = makeRequestAndReturnString(alexaServer + "/api/notifications"); JsonNotificationsResponse result = parseJson(response, JsonNotificationsResponse.class); + if (result == null) { + return new JsonNotificationResponse[0]; + } JsonNotificationResponse[] notifications = result.notifications; if (notifications == null) { return new JsonNotificationResponse[0]; @@ -1342,7 +1508,7 @@ public JsonNotificationResponse[] notifications() throws IOException, URISyntaxE return notifications; } - public JsonNotificationResponse notification(Device device, String type, @Nullable String label, + public @Nullable JsonNotificationResponse notification(Device device, String type, @Nullable String label, @Nullable JsonNotificationSound sound) throws IOException, URISyntaxException { Date date = new Date(new Date().getTime()); long createdDate = date.getTime(); @@ -1374,7 +1540,7 @@ public void stopNotification(JsonNotificationResponse notification) throws IOExc makeRequestAndReturnString("DELETE", alexaServer + "/api/notifications/" + notification.id, null, true, null); } - public JsonNotificationResponse getNotificationState(JsonNotificationResponse notification) + public @Nullable JsonNotificationResponse getNotificationState(JsonNotificationResponse notification) throws IOException, URISyntaxException { String response = makeRequestAndReturnString("GET", alexaServer + "/api/notifications/" + notification.id, null, true, null); @@ -1411,22 +1577,24 @@ public void playMusicVoiceCommand(Device device, String providerId, String voice String playloadString = gson.toJson(payload); - JsonObject postValidataionJson = new JsonObject(); + JsonObject postValidationJson = new JsonObject(); - postValidataionJson.addProperty("type", "Alexa.Music.PlaySearchPhrase"); - postValidataionJson.addProperty("operationPayload", playloadString); + postValidationJson.addProperty("type", "Alexa.Music.PlaySearchPhrase"); + postValidationJson.addProperty("operationPayload", playloadString); - String postDataValidate = postValidataionJson.toString(); + String postDataValidate = postValidationJson.toString(); String validateResultJson = makeRequestAndReturnString("POST", alexaServer + "/api/behaviors/operation/validate", postDataValidate, true, null); if (StringUtils.isNotEmpty(validateResultJson)) { JsonPlayValidationResult validationResult = parseJson(validateResultJson, JsonPlayValidationResult.class); - JsonPlaySearchPhraseOperationPayload validatedOperationPayload = validationResult.operationPayload; - if (validatedOperationPayload != null) { - payload.sanitizedSearchPhrase = validatedOperationPayload.sanitizedSearchPhrase; - payload.searchPhrase = validatedOperationPayload.searchPhrase; + if (validationResult != null) { + JsonPlaySearchPhraseOperationPayload validatedOperationPayload = validationResult.operationPayload; + if (validatedOperationPayload != null) { + payload.sanitizedSearchPhrase = validatedOperationPayload.sanitizedSearchPhrase; + payload.searchPhrase = validatedOperationPayload.searchPhrase; + } } } @@ -1450,13 +1618,13 @@ public void playMusicVoiceCommand(Device device, String providerId, String voice makeRequest("POST", alexaServer + "/api/behaviors/preview", postData, true, true, null, 3); } - public JsonEqualizer getEqualizer(Device device) throws IOException, URISyntaxException { + public @Nullable JsonEqualizer getEqualizer(Device device) throws IOException, URISyntaxException { String json = makeRequestAndReturnString( alexaServer + "/api/equalizer/" + device.serialNumber + "/" + device.deviceType); return parseJson(json, JsonEqualizer.class); } - public void SetEqualizer(Device device, JsonEqualizer settings) throws IOException, URISyntaxException { + public void setEqualizer(Device device, JsonEqualizer settings) throws IOException, URISyntaxException { String postData = gson.toJson(settings); makeRequest("POST", alexaServer + "/api/equalizer/" + device.serialNumber + "/" + device.deviceType, postData, true, true, null, 0); diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java index f99797e10d609..4d6c9e5b05f63 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java @@ -158,7 +158,7 @@ public void close() { } logger.trace("Connect future = {}", sessionFuture); final Future sessionFuture = this.sessionFuture; - if (!sessionFuture.isDone()) { + if (sessionFuture != null && !sessionFuture.isDone()) { sessionFuture.cancel(true); } try { @@ -587,9 +587,7 @@ byte[] encodePing() { byte[] payload = "Regular".getBytes(StandardCharsets.US_ASCII); // g = h.length byte[] bufferPing = new byte[header.length + 4 + 8 + 4 + 2 * payload.length]; int idx = 0; - for (int q = 0; q < header.length; q++) { - bufferPing[q] = header[q]; - } + System.arraycopy(header, 0, bufferPing, 0, header.length); idx += header.length; encode(bufferPing, 0, idx, 4); idx += 4; diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandler.java index 1c947956975b4..f64dc05a691bb 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandler.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandler.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.net.URISyntaxException; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.types.Command; import org.openhab.binding.amazonechocontrol.internal.Connection; @@ -30,6 +31,7 @@ * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public abstract class ChannelHandler { public abstract boolean tryHandleCommand(Device device, Connection connection, String channelId, Command command) throws IOException, URISyntaxException; diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerAnnouncement.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerAnnouncement.java index 89af22d404a7f..8b59e4f07b04c 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerAnnouncement.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerAnnouncement.java @@ -16,6 +16,7 @@ import java.net.URISyntaxException; import org.apache.commons.lang.StringEscapeUtils; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.library.types.StringType; import org.eclipse.smarthome.core.types.Command; @@ -30,6 +31,7 @@ * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class ChannelHandlerAnnouncement extends ChannelHandler { private static final String CHANNEL_NAME = "announcement"; @@ -89,16 +91,16 @@ public boolean tryHandleCommand(Device device, Connection connection, String cha } thingHandler.startAnnouncment(device, speak, body, title, volume); } - RefreshChannel(); + refreshChannel(); } return false; } - void RefreshChannel() { + private void refreshChannel() { thingHandler.updateChannelState(CHANNEL_NAME, new StringType("")); } - static class AnnouncementRequestJson { + private static class AnnouncementRequestJson { public @Nullable Boolean sound; public @Nullable String title; public @Nullable String body; diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerSendMessage.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerSendMessage.java index 5d1be525be3bd..b8be3997c9c80 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerSendMessage.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerSendMessage.java @@ -16,6 +16,7 @@ import java.net.URISyntaxException; import java.time.LocalDateTime; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.library.types.StringType; import org.eclipse.smarthome.core.types.Command; @@ -29,6 +30,7 @@ * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class ChannelHandlerSendMessage extends ChannelHandler { private static final String CHANNEL_NAME = "sendMessage"; private @Nullable AccountJson accountJson; @@ -49,7 +51,7 @@ public boolean tryHandleCommand(Device device, Connection connection, String cha AccountJson currentAccountJson = this.accountJson; if (currentAccountJson == null) { String accountResult = connection.makeRequestAndReturnString(baseUrl + "/accounts"); - AccountJson[] accountsJson = this.gson.fromJson(accountResult, AccountJson[].class); + AccountJson @Nullable [] accountsJson = gson.fromJson(accountResult, AccountJson[].class); if (accountsJson == null) { return false; } @@ -85,16 +87,17 @@ public boolean tryHandleCommand(Device device, Connection connection, String cha + "/messages"; connection.makeRequestAndReturnString("POST", sendUrl, sendConversationBody, true, null); } - RefreshChannel(); + refreshChannel(); } return false; } - void RefreshChannel() { + private void refreshChannel() { thingHandler.updateChannelState(CHANNEL_NAME, new StringType("")); } - static class AccountJson { + @SuppressWarnings("unused") + private static class AccountJson { public @Nullable String commsId; public @Nullable String directedId; public @Nullable String phoneCountryCode; @@ -110,7 +113,8 @@ static class AccountJson { public @Nullable Boolean speakerProvisioned; } - static class SendConversationJson { + @SuppressWarnings("unused") + private static class SendConversationJson { public @Nullable String conversationId; public @Nullable String clientMessageId; public @Nullable Integer messageId; @@ -120,7 +124,7 @@ static class SendConversationJson { public Payload payload = new Payload(); public Integer status = 1; - static class Payload { + private static class Payload { public @Nullable String text; } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index 1ba4d01d21429..da91ce1dbe351 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -12,7 +12,15 @@ */ package org.openhab.binding.amazonechocontrol.internal.discovery; -import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.DEVICE_PROPERTY_FAMILY; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.DEVICE_PROPERTY_SERIAL_NUMBER; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.SUPPORTED_ECHO_THING_TYPES_UIDS; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.THING_TYPE_ECHO; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.THING_TYPE_ECHO_SHOW; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.THING_TYPE_ECHO_SPOT; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.THING_TYPE_ECHO_WHA; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.THING_TYPE_FLASH_BRIEFING_PROFILE; import java.util.Date; import java.util.HashMap; @@ -28,6 +36,8 @@ import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; import org.eclipse.smarthome.config.discovery.DiscoveryResult; import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.config.discovery.DiscoveryServiceCallback; +import org.eclipse.smarthome.config.discovery.ExtendedDiscoveryService; import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.ThingUID; import org.openhab.binding.amazonechocontrol.internal.Connection; @@ -44,17 +54,24 @@ * @author Michael Geramb - Initial contribution */ @NonNullByDefault -public class AmazonEchoDiscovery extends AbstractDiscoveryService { +public class AmazonEchoDiscovery extends AbstractDiscoveryService implements ExtendedDiscoveryService { - private final AccountHandler accountHandler; + AccountHandler accountHandler; private final Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class); private final Set discoverdFlashBriefings = new HashSet<>(); + private @Nullable DiscoveryServiceCallback discoveryServiceCallback; + private @Nullable ScheduledFuture startScanStateJob; - private long activateTimeStamp; + private @Nullable Long activateTimeStamp; + + @Override + public void setDiscoveryServiceCallback(DiscoveryServiceCallback discoveryServiceCallback) { + this.discoveryServiceCallback = discoveryServiceCallback; + } public AmazonEchoDiscovery(AccountHandler accountHandler) { - super(SUPPORTED_THING_TYPES_UIDS, 10); + super(SUPPORTED_ECHO_THING_TYPES_UIDS, 10); this.accountHandler = accountHandler; } @@ -70,8 +87,10 @@ public void deactivate() { @Override protected void startScan() { stopScanJob(); - removeOlderResults(activateTimeStamp); - + final Long activateTimeStamp = this.activateTimeStamp; + if (activateTimeStamp != null) { + removeOlderResults(activateTimeStamp); + } setDevices(accountHandler.updateDeviceList()); String currentFlashBriefingConfiguration = accountHandler.getNewCurrentFlashbriefingConfiguration(); @@ -109,7 +128,7 @@ protected void stopBackgroundDiscovery() { stopScanJob(); } - private void stopScanJob() { + void stopScanJob() { @Nullable ScheduledFuture currentStartScanStateJob = startScanStateJob; if (currentStartScanStateJob != null) { @@ -125,10 +144,16 @@ public void activate(@Nullable Map config) { if (config != null) { modified(config); } - activateTimeStamp = new Date().getTime(); + if (activateTimeStamp == null) { + activateTimeStamp = new Date().getTime(); + } } synchronized void setDevices(List deviceList) { + DiscoveryServiceCallback discoveryServiceCallback = this.discoveryServiceCallback; + if (discoveryServiceCallback == null) { + return; + } for (Device device : deviceList) { String serialNumber = device.serialNumber; if (serialNumber != null) { @@ -150,7 +175,12 @@ synchronized void setDevices(List deviceList) { ThingUID brigdeThingUID = this.accountHandler.getThing().getUID(); ThingUID thingUID = new ThingUID(thingTypeId, brigdeThingUID, serialNumber); - + if (discoveryServiceCallback.getExistingDiscoveryResult(thingUID) != null) { + continue; + } + if (discoveryServiceCallback.getExistingThing(thingUID) != null) { + continue; + } DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel(device.accountName) .withProperty(DEVICE_PROPERTY_SERIAL_NUMBER, serialNumber) .withProperty(DEVICE_PROPERTY_FAMILY, deviceFamily) @@ -170,13 +200,30 @@ public synchronized void discoverFlashBriefingProfiles(String currentFlashBriefi if (currentFlashBriefingJson.isEmpty()) { return; } + DiscoveryServiceCallback discoveryServiceCallback = this.discoveryServiceCallback; + if (discoveryServiceCallback == null) { + return; + } if (!discoverdFlashBriefings.contains(currentFlashBriefingJson)) { - ThingUID brigdeThingUID = this.accountHandler.getThing().getUID(); - ThingUID thingUID = new ThingUID(THING_TYPE_FLASH_BRIEFING_PROFILE, brigdeThingUID, - Integer.toString(currentFlashBriefingJson.hashCode())); - - DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("FlashBriefing") + ThingUID freeThingUID = null; + int freeIndex = 0; + for (int i = 1; i < 1000; i++) { + String id = Integer.toString(i); + ThingUID brigdeThingUID = this.accountHandler.getThing().getUID(); + ThingUID thingUID = new ThingUID(THING_TYPE_FLASH_BRIEFING_PROFILE, brigdeThingUID, id); + if (discoveryServiceCallback.getExistingThing(thingUID) == null + && discoveryServiceCallback.getExistingDiscoveryResult(thingUID) == null) { + freeThingUID = thingUID; + freeIndex = i; + break; + } + } + if (freeThingUID == null) { + logger.debug("No more free flashbriefing thing ID found"); + return; + } + DiscoveryResult result = DiscoveryResultBuilder.create(freeThingUID).withLabel("FlashBriefing " + freeIndex) .withProperty(DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE, currentFlashBriefingJson) .withBridge(accountHandler.getThing().getUID()).build(); logger.debug("Flash Briefing {} discovered", currentFlashBriefingJson); diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java new file mode 100644 index 0000000000000..ba48397429fea --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java @@ -0,0 +1,247 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.discovery; + +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.DEVICE_PROPERTY_ID; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.SUPPORTED_SMART_HOME_THING_TYPES_UIDS; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.THING_TYPE_SMART_HOME_DEVICE; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.THING_TYPE_SMART_HOME_DEVICE_GROUP; + +import java.util.Date; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; +import org.eclipse.smarthome.config.discovery.DiscoveryResult; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.config.discovery.DiscoveryServiceCallback; +import org.eclipse.smarthome.config.discovery.ExtendedDiscoveryService; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler; +import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.DriverIdentity; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroups.SmartHomeGroup; +import org.openhab.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice; +import org.openhab.binding.amazonechocontrol.internal.smarthome.Constants; +import org.osgi.service.component.annotations.Activate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Lukas Knoeller - Initial contribution + */ +@NonNullByDefault +public class SmartHomeDevicesDiscovery extends AbstractDiscoveryService implements ExtendedDiscoveryService { + private AccountHandler accountHandler; + private final Logger logger = LoggerFactory.getLogger(SmartHomeDevicesDiscovery.class); + + private @Nullable ScheduledFuture startScanStateJob; + private @Nullable Long activateTimeStamp; + + private @Nullable DiscoveryServiceCallback discoveryServiceCallback; + + @Override + public void setDiscoveryServiceCallback(DiscoveryServiceCallback discoveryServiceCallback) { + this.discoveryServiceCallback = discoveryServiceCallback; + } + + public SmartHomeDevicesDiscovery(AccountHandler accountHandler) { + super(SUPPORTED_SMART_HOME_THING_TYPES_UIDS, 10); + this.accountHandler = accountHandler; + } + + public void activate() { + activate(new Hashtable()); + } + + @Override + public void deactivate() { + super.deactivate(); + } + + @Override + protected void startScan() { + stopScanJob(); + Long activateTimeStamp = this.activateTimeStamp; + if (activateTimeStamp != null) { + removeOlderResults(activateTimeStamp); + } + setSmartHomeDevices(accountHandler.updateSmartHomeDeviceList(false)); + } + + protected void startAutomaticScan() { + if (!this.accountHandler.getThing().getThings().isEmpty()) { + stopScanJob(); + return; + } + Connection connection = this.accountHandler.findConnection(); + if (connection == null) { + return; + } + Date verifyTime = connection.tryGetVerifyTime(); + if (verifyTime == null) { + return; + } + if (new Date().getTime() - verifyTime.getTime() < 10000) { + return; + } + startScan(); + } + + @Override + protected void startBackgroundDiscovery() { + stopScanJob(); + startScanStateJob = scheduler.scheduleWithFixedDelay(this::startAutomaticScan, 3000, 1000, + TimeUnit.MILLISECONDS); + } + + @Override + protected void stopBackgroundDiscovery() { + stopScanJob(); + } + + void stopScanJob() { + ScheduledFuture currentStartScanStateJob = startScanStateJob; + if (currentStartScanStateJob != null) { + currentStartScanStateJob.cancel(false); + startScanStateJob = null; + } + super.stopScan(); + } + + @Override + @Activate + public void activate(@Nullable Map config) { + super.activate(config); + if (config != null) { + modified(config); + } + Long activateTimeStamp = this.activateTimeStamp; + if (activateTimeStamp == null) { + this.activateTimeStamp = new Date().getTime(); + } + }; + + synchronized void setSmartHomeDevices(List deviceList) { + DiscoveryServiceCallback discoveryServiceCallback = this.discoveryServiceCallback; + + if (discoveryServiceCallback == null) { + return; + } + int smartHomeDeviceDiscoveryMode = accountHandler.getSmartHomeDevicesDiscoveryMode(); + if (smartHomeDeviceDiscoveryMode == 0) { + return; + } + + for (Object smartHomeDevice : deviceList) { + ThingUID bridgeThingUID = this.accountHandler.getThing().getUID(); + ThingUID thingUID = null; + String deviceName = null; + Map props = new HashMap<>(); + + if (smartHomeDevice instanceof SmartHomeDevice) { + SmartHomeDevice shd = (SmartHomeDevice) smartHomeDevice; + + String entityId = shd.entityId; + if (entityId == null) { + // No entity id + continue; + } + String id = shd.findId(); + if (id == null) { + // No id + continue; + } + boolean isSkillDevice = false; + DriverIdentity driverIdentity = shd.driverIdentity; + isSkillDevice = driverIdentity != null && "SKILL".equals(driverIdentity.namespace); + + if (smartHomeDeviceDiscoveryMode == 1 && isSkillDevice) { + // Connected through skill + continue; + } + if (!(smartHomeDeviceDiscoveryMode == 2) && "openHAB".equalsIgnoreCase(shd.manufacturerName)) { + // OpenHAB device + continue; + } + + if (Stream.of(shd.capabilities).noneMatch(capability -> capability != null + && Constants.SUPPORTED_INTERFACES.contains(capability.interfaceName))) { + // No supported interface found + continue; + } + + thingUID = new ThingUID(THING_TYPE_SMART_HOME_DEVICE, bridgeThingUID, entityId.replace(".", "-")); + + if ("Amazon".equals(shd.manufacturerName) && driverIdentity != null + && "SonarCloudService".equals(driverIdentity.identifier)) { + deviceName = "Alexa Guard on " + shd.friendlyName; + } else if ("Amazon".equals(shd.manufacturerName) && driverIdentity != null + && "OnGuardSmartHomeBridgeService".equals(driverIdentity.identifier)) { + deviceName = "Alexa Guard"; + } else if (shd.aliases != null && shd.aliases.length > 0 && shd.aliases[0] != null + && shd.aliases[0].friendlyName != null) { + deviceName = shd.aliases[0].friendlyName; + } else { + deviceName = shd.friendlyName; + } + props.put(DEVICE_PROPERTY_ID, id); + } + + if (smartHomeDevice instanceof SmartHomeGroup) { + SmartHomeGroup shg = (SmartHomeGroup) smartHomeDevice; + String id = shg.findId(); + if (id == null) { + // No id + continue; + } + Set supportedChildren = SmartHomeDeviceHandler.getSupportedSmartHomeDevices(shg, + deviceList); + if (supportedChildren.size() == 0) { + // No children with an supported interface + continue; + } + thingUID = new ThingUID(THING_TYPE_SMART_HOME_DEVICE_GROUP, bridgeThingUID, id.replace(".", "-")); + deviceName = shg.applianceGroupName; + props.put(DEVICE_PROPERTY_ID, id); + } + + if (thingUID != null) { + if (discoveryServiceCallback.getExistingDiscoveryResult(thingUID) != null) { + continue; + } + + if (discoveryServiceCallback.getExistingThing(thingUID) != null) { + continue; + } + + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel(deviceName) + .withProperties(props).withBridge(bridgeThingUID).build(); + + logger.debug("Device [{}] found.", deviceName); + + thingDiscovered(result); + } + } + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java index b7ef5d9207156..ee9f443306c06 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java @@ -18,11 +18,14 @@ import java.net.UnknownHostException; import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -41,6 +44,7 @@ import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.amazonechocontrol.internal.AccountHandlerConfig; import org.openhab.binding.amazonechocontrol.internal.AccountServlet; import org.openhab.binding.amazonechocontrol.internal.Connection; import org.openhab.binding.amazonechocontrol.internal.ConnectionException; @@ -68,12 +72,16 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPushCommand; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonWakeWords.WakeWord; +import org.openhab.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice; +import org.openhab.binding.amazonechocontrol.internal.smarthome.SmartHomeDeviceStateGroupUpdateCalculator; import org.osgi.service.http.HttpService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.JsonArray; import com.google.gson.JsonSyntaxException; /** @@ -83,26 +91,38 @@ */ @NonNullByDefault public class AccountHandler extends BaseBridgeHandler implements IWebSocketCommandHandler, IAmazonThingHandler { - private final Logger logger = LoggerFactory.getLogger(AccountHandler.class); private Storage stateStorage; private @Nullable Connection connection; private @Nullable WebSocketConnection webSocketConnection; - private final Set echoHandlers = new HashSet<>(); - private final Set flashBriefingProfileHandlers = new HashSet<>(); + + private final Set echoHandlers = new CopyOnWriteArraySet<>(); + private final Set smartHomeDeviceHandlers = new CopyOnWriteArraySet<>(); + private final Set flashBriefingProfileHandlers = new CopyOnWriteArraySet<>(); + private final Object synchronizeConnection = new Object(); private Map jsonSerialNumberDeviceMapping = new HashMap<>(); + private Map jsonIdSmartHomeDeviceMapping = new HashMap<>(); + private Map jsonSerialNumberSmartHomeDeviceMapping = new HashMap<>(); + private @Nullable ScheduledFuture checkDataJob; private @Nullable ScheduledFuture checkLoginJob; + private @Nullable ScheduledFuture updateSmartHomeStateJob; private @Nullable ScheduledFuture refreshAfterCommandJob; - private @Nullable ScheduledFuture foceCheckDataJob; + private @Nullable ScheduledFuture refreshSmartHomeAfterCommandJob; + private final Object synchronizeSmartHomeJobScheduler = new Object(); + private @Nullable ScheduledFuture forceCheckDataJob; private String currentFlashBriefingJson = ""; private final HttpService httpService; private @Nullable AccountServlet accountServlet; private final Gson gson; - int checkDataCounter; + private int checkDataCounter; + private final LinkedBlockingQueue requestedDeviceUpdates = new LinkedBlockingQueue<>(); + private @Nullable SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator; private List channelHandlers = new ArrayList<>(); + private AccountHandlerConfig handlerConfig = new AccountHandlerConfig(); + public AccountHandler(Bridge bridge, HttpService httpService, Storage stateStorage, Gson gson) { super(bridge); this.gson = gson; @@ -113,7 +133,7 @@ public AccountHandler(Bridge bridge, HttpService httpService, Storage st @Override public void initialize() { - logger.debug("amazon account bridge starting..."); + handlerConfig = getConfig().as(AccountHandlerConfig.class); synchronized (synchronizeConnection) { Connection connection = this.connection; @@ -121,8 +141,13 @@ public void initialize() { this.connection = new Connection(null, gson); } } - if (this.accountServlet == null) { - this.accountServlet = new AccountServlet(httpService, this.getThing().getUID().getId(), this, gson); + + if (accountServlet == null) { + try { + accountServlet = new AccountServlet(httpService, this.getThing().getUID().getId(), this, gson); + } catch (IllegalStateException e) { + logger.warn("Failed to create account servlet", e); + } } updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login"); @@ -130,7 +155,18 @@ public void initialize() { checkLoginJob = scheduler.scheduleWithFixedDelay(this::checkLogin, 0, 60, TimeUnit.SECONDS); checkDataJob = scheduler.scheduleWithFixedDelay(this::checkData, 4, 60, TimeUnit.SECONDS); - logger.debug("amazon account bridge handler started."); + int pollingIntervalAlexa = handlerConfig.pollingIntervalSmartHomeAlexa; + if (pollingIntervalAlexa < 10) { + pollingIntervalAlexa = 10; + } + int pollingIntervalSkills = handlerConfig.pollingIntervalSmartSkills; + if (pollingIntervalSkills < 60) { + pollingIntervalSkills = 60; + } + smartHomeDeviceStateGroupUpdateCalculator = new SmartHomeDeviceStateGroupUpdateCalculator(pollingIntervalAlexa, + pollingIntervalSkills); + updateSmartHomeStateJob = scheduler.scheduleWithFixedDelay(() -> updateSmartHomeState(null), 20, 10, + TimeUnit.SECONDS); } @Override @@ -162,30 +198,34 @@ public void handleCommand(ChannelUID channelUID, Command command) { } public List getFlashBriefingProfileHandlers() { - return new ArrayList<>(this.flashBriefingProfileHandlers); + return new ArrayList<>(flashBriefingProfileHandlers); } public List getLastKnownDevices() { return new ArrayList<>(jsonSerialNumberDeviceMapping.values()); } + public List getLastKnownSmartHomeDevices() { + return new ArrayList<>(jsonIdSmartHomeDeviceMapping.values()); + } + public void addEchoHandler(EchoHandler echoHandler) { - synchronized (echoHandlers) { - if (!echoHandlers.add(echoHandler)) { - return; - } + if (echoHandlers.add(echoHandler)) { + + forceCheckData(); } - forceCheckData(); } - public void forceCheckData() { - if (foceCheckDataJob == null) { - foceCheckDataJob = scheduler.schedule(this::forceCheckDataHandler, 1000, TimeUnit.MILLISECONDS); + public void addSmartHomeDeviceHandler(SmartHomeDeviceHandler smartHomeDeviceHandler) { + if (smartHomeDeviceHandlers.add(smartHomeDeviceHandler)) { + forceCheckData(); } } - void forceCheckDataHandler() { - this.checkData(); + public void forceCheckData() { + if (forceCheckDataJob == null) { + forceCheckDataJob = scheduler.schedule(this::checkData, 1000, TimeUnit.MILLISECONDS); + } } public @Nullable Thing findThingBySerialNumber(@Nullable String deviceSerialNumber) { @@ -197,20 +237,16 @@ void forceCheckDataHandler() { } public @Nullable EchoHandler findEchoHandlerBySerialNumber(@Nullable String deviceSerialNumber) { - synchronized (echoHandlers) { - for (EchoHandler echoHandler : echoHandlers) { - if (StringUtils.equals(echoHandler.findSerialNumber(), deviceSerialNumber)) { - return echoHandler; - } + for (EchoHandler echoHandler : echoHandlers) { + if (deviceSerialNumber != null && deviceSerialNumber.equals(echoHandler.findSerialNumber())) { + return echoHandler; } } return null; } public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBriefingProfileHandler) { - synchronized (flashBriefingProfileHandlers) { - flashBriefingProfileHandlers.add(flashBriefingProfileHandler); - } + flashBriefingProfileHandlers.add(flashBriefingProfileHandler); Connection connection = this.connection; if (connection != null && connection.getIsLoggedIn()) { if (currentFlashBriefingJson.isEmpty()) { @@ -240,15 +276,15 @@ public void handleRemoval() { public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) { // check for echo handler if (childHandler instanceof EchoHandler) { - synchronized (echoHandlers) { - echoHandlers.remove(childHandler); - } + echoHandlers.remove(childHandler); } // check for flash briefing profile handler if (childHandler instanceof FlashBriefingProfileHandler) { - synchronized (flashBriefingProfileHandlers) { - flashBriefingProfileHandlers.remove(childHandler); - } + flashBriefingProfileHandlers.remove(childHandler); + } + // check for flash briefing profile handler + if (childHandler instanceof SmartHomeDeviceHandler) { + smartHomeDeviceHandlers.remove(childHandler); } super.childHandlerDisposed(childHandler, childThing); } @@ -266,30 +302,36 @@ public void dispose() { private void cleanup() { logger.debug("cleanup {}", getThing().getUID().getAsString()); - @Nullable + ScheduledFuture updateSmartHomeStateJob = this.updateSmartHomeStateJob; + if (updateSmartHomeStateJob != null) { + updateSmartHomeStateJob.cancel(true); + this.updateSmartHomeStateJob = null; + } ScheduledFuture refreshJob = this.checkDataJob; if (refreshJob != null) { refreshJob.cancel(true); this.checkDataJob = null; } - @Nullable ScheduledFuture refreshLogin = this.checkLoginJob; if (refreshLogin != null) { refreshLogin.cancel(true); this.checkLoginJob = null; } - @Nullable - ScheduledFuture foceCheckDataJob = this.foceCheckDataJob; + ScheduledFuture foceCheckDataJob = this.forceCheckDataJob; if (foceCheckDataJob != null) { foceCheckDataJob.cancel(true); - this.foceCheckDataJob = null; + this.forceCheckDataJob = null; } - @Nullable - ScheduledFuture refreshDataDelayed = this.refreshAfterCommandJob; - if (refreshDataDelayed != null) { - refreshDataDelayed.cancel(true); + ScheduledFuture refreshAfterCommandJob = this.refreshAfterCommandJob; + if (refreshAfterCommandJob != null) { + refreshAfterCommandJob.cancel(true); this.refreshAfterCommandJob = null; } + ScheduledFuture refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob; + if (refreshSmartHomeAfterCommandJob != null) { + refreshSmartHomeAfterCommandJob.cancel(true); + this.refreshSmartHomeAfterCommandJob = null; + } Connection connection = this.connection; if (connection != null) { connection.logout(); @@ -341,7 +383,6 @@ private void checkLogin() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); } } - } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail. logger.error("check login fails with unexpected error", e); } @@ -360,6 +401,7 @@ public void setConnection(@Nullable Connection connection) { closeWebSocketConnection(); if (connection != null) { updateDeviceList(); + updateSmartHomeDeviceList(false); updateFlashBriefingHandlers(); updateStatus(ThingStatus.ONLINE); scheduleUpdate(); @@ -375,7 +417,7 @@ void closeWebSocketConnection() { } } - boolean checkWebSocketConnection() { + private boolean checkWebSocketConnection() { WebSocketConnection webSocketConnection = this.webSocketConnection; if (webSocketConnection == null || webSocketConnection.isClosed()) { Connection connection = this.connection; @@ -398,9 +440,9 @@ private void checkData() { Connection connection = this.connection; if (connection != null && connection.getIsLoggedIn()) { checkDataCounter++; - if (checkDataCounter > 60 || foceCheckDataJob != null) { + if (checkDataCounter > 60 || forceCheckDataJob != null) { checkDataCounter = 0; - foceCheckDataJob = null; + forceCheckDataJob = null; } if (!checkWebSocketConnection() || checkDataCounter == 0) { refreshData(); @@ -433,9 +475,8 @@ private void refreshNotifications(@Nullable JsonCommandPayloadPushNotificationCh } ZonedDateTime timeStampNow = ZonedDateTime.now(); - for (EchoHandler child : echoHandlers) { - child.updateNotifications(timeStamp, timeStampNow, pushPayload, notifications); - } + echoHandlers.forEach( + echoHandler -> echoHandler.updateNotifications(timeStamp, timeStampNow, pushPayload, notifications)); } private void refreshData() { @@ -457,6 +498,7 @@ private void refreshData() { // get all devices registered in the account updateDeviceList(); + updateSmartHomeDeviceList(false); updateFlashBriefingHandlers(); DeviceNotificationState[] deviceNotificationStates = null; @@ -484,9 +526,8 @@ private void refreshData() { } // forward device information to echo handler for (EchoHandler child : echoHandlers) { - Device device = findDeviceJson(child); + Device device = findDeviceJson(child.findSerialNumber()); - @Nullable JsonNotificationSound[] notificationSounds = null; JsonPlaylists playlists = null; if (device != null && currentConnection.getIsLoggedIn()) { @@ -548,11 +589,6 @@ private void refreshData() { } } - public @Nullable Device findDeviceJson(EchoHandler echoHandler) { - String serialNumber = echoHandler.findSerialNumber(); - return findDeviceJson(serialNumber); - } - public @Nullable Device findDeviceJson(@Nullable String serialNumber) { Device result = null; if (StringUtils.isNotEmpty(serialNumber)) { @@ -594,6 +630,7 @@ public List updateDeviceList() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); } if (devices != null) { + // create new device map Map newJsonSerialDeviceMapping = new HashMap<>(); for (Device device : devices) { String serialNumber = device.serialNumber; @@ -604,27 +641,27 @@ public List updateDeviceList() { } jsonSerialNumberDeviceMapping = newJsonSerialDeviceMapping; } - WakeWord[] wakeWords = currentConnection.getWakeWords(); - synchronized (echoHandlers) { - for (EchoHandler child : echoHandlers) { - String serialNumber = child.findSerialNumber(); - String deviceWakeWord = null; - for (WakeWord wakeWord : wakeWords) { - if (wakeWord != null) { - if (StringUtils.equals(wakeWord.deviceSerialNumber, serialNumber)) { - deviceWakeWord = wakeWord.wakeWord; - break; - } + WakeWord[] wakeWords = currentConnection.getWakeWords(); + // update handlers + for (EchoHandler echoHandler : echoHandlers) { + String serialNumber = echoHandler.findSerialNumber(); + String deviceWakeWord = null; + for (WakeWord wakeWord : wakeWords) { + if (wakeWord != null) { + if (serialNumber != null && serialNumber.equals(wakeWord.deviceSerialNumber)) { + deviceWakeWord = wakeWord.wakeWord; + break; } } - child.setDeviceAndUpdateThingState(this, findDeviceJson(child), deviceWakeWord); } + echoHandler.setDeviceAndUpdateThingState(this, findDeviceJson(serialNumber), deviceWakeWord); } + if (devices != null) { return devices; } - return new ArrayList<>(); + return Collections.emptyList(); } public void setEnabledFlashBriefingsJson(String flashBriefingJson) { @@ -653,19 +690,17 @@ public String updateFlashBriefingHandlers() { } private String updateFlashBriefingHandlers(Connection currentConnection) { - synchronized (flashBriefingProfileHandlers) { - if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) { - updateFlashBriefingProfiles(currentConnection); - } - boolean flashBriefingProfileFound = false; - for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) { - flashBriefingProfileFound |= child.initialize(this, currentFlashBriefingJson); - } - if (flashBriefingProfileFound) { - return ""; - } - return this.currentFlashBriefingJson; + if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) { + updateFlashBriefingProfiles(currentConnection); } + boolean flashBriefingProfileFound = false; + for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) { + flashBriefingProfileFound |= child.initialize(this, currentFlashBriefingJson); + } + if (flashBriefingProfileFound) { + return ""; + } + return this.currentFlashBriefingJson; } public @Nullable Connection findConnection() { @@ -751,7 +786,6 @@ void handleWebsocketCommand(JsonPushCommand pushCommand) { } private void handlePushDeviceCommand(DopplerId dopplerId, String command, String payload) { - @Nullable EchoHandler echoHandler = findEchoHandlerBySerialNumber(dopplerId.deviceSerialNumber); if (echoHandler != null) { echoHandler.handlePushCommand(command, payload); @@ -800,4 +834,163 @@ private void handlePushActivity(@Nullable String payload) { void refreshAfterCommand() { refreshData(); } + + private @Nullable SmartHomeBaseDevice findSmartDeviceHomeJson(SmartHomeDeviceHandler handler) { + String id = handler.getId(); + if (!id.isEmpty()) { + return jsonIdSmartHomeDeviceMapping.get(id); + } + return null; + } + + public int getSmartHomeDevicesDiscoveryMode() { + return handlerConfig.discoverSmartHome; + } + + public List updateSmartHomeDeviceList(boolean forceUpdate) { + Connection currentConnection = connection; + if (currentConnection == null) { + return Collections.emptyList(); + } + + if (!forceUpdate && smartHomeDeviceHandlers.isEmpty() && getSmartHomeDevicesDiscoveryMode() == 0) { + return Collections.emptyList(); + } + + List smartHomeDevices = null; + try { + if (currentConnection.getIsLoggedIn()) { + smartHomeDevices = currentConnection.getSmarthomeDeviceList(); + } + } catch (IOException | URISyntaxException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); + } + if (smartHomeDevices != null) { + // create new id map + Map newJsonIdSmartHomeDeviceMapping = new HashMap<>(); + for (Object smartHomeDevice : smartHomeDevices) { + if (smartHomeDevice instanceof SmartHomeBaseDevice) { + SmartHomeBaseDevice smartHomeBaseDevice = (SmartHomeBaseDevice) smartHomeDevice; + String id = smartHomeBaseDevice.findId(); + if (id != null) { + newJsonIdSmartHomeDeviceMapping.put(id, smartHomeBaseDevice); + } + } + } + jsonIdSmartHomeDeviceMapping = newJsonIdSmartHomeDeviceMapping; + } + // update handlers + smartHomeDeviceHandlers + .forEach(child -> child.setDeviceAndUpdateThingState(this, findSmartDeviceHomeJson(child))); + + if (smartHomeDevices != null) { + Map newJsonSerialNumberSmartHomeDeviceMapping = new HashMap<>(); + for (Object smartDevice : smartHomeDevices) { + if (smartDevice instanceof SmartHomeDevice) { + SmartHomeDevice shd = (SmartHomeDevice) smartDevice; + String entityId = shd.entityId; + if (entityId != null) { + newJsonSerialNumberSmartHomeDeviceMapping.put(entityId, shd); + } + } + } + jsonSerialNumberSmartHomeDeviceMapping = newJsonSerialNumberSmartHomeDeviceMapping; + } + if (smartHomeDevices != null) { + return smartHomeDevices; + } + + return Collections.emptyList(); + } + + public void forceDelayedSmartHomeStateUpdate(@Nullable String deviceId) { + if (deviceId == null) { + return; + } + synchronized (synchronizeSmartHomeJobScheduler) { + requestedDeviceUpdates.add(deviceId); + ScheduledFuture refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob; + if (refreshSmartHomeAfterCommandJob != null) { + refreshSmartHomeAfterCommandJob.cancel(false); + } + this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 500, + TimeUnit.MILLISECONDS); + } + } + + private void updateSmartHomeStateJob() { + Set deviceUpdates = new HashSet<>(); + + synchronized (synchronizeSmartHomeJobScheduler) { + Connection connection = this.connection; + if (connection == null || !connection.getIsLoggedIn()) { + this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 1000, + TimeUnit.MILLISECONDS); + return; + } + requestedDeviceUpdates.drainTo(deviceUpdates); + this.refreshSmartHomeAfterCommandJob = null; + } + + deviceUpdates.forEach(this::updateSmartHomeState); + } + + private synchronized void updateSmartHomeState(@Nullable String deviceFilterId) { + try { + logger.debug("updateSmartHomeState started"); + Connection connection = this.connection; + if (connection == null || !connection.getIsLoggedIn()) { + return; + } + List allDevices = getLastKnownSmartHomeDevices(); + Set applianceIds = new HashSet<>(); + if (deviceFilterId != null) { + applianceIds.add(deviceFilterId); + } else { + SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator = this.smartHomeDeviceStateGroupUpdateCalculator; + if (smartHomeDeviceStateGroupUpdateCalculator == null) { + return; + } + if (smartHomeDeviceHandlers.isEmpty()) { + return; + } + List devicesToUpdate = new ArrayList<>(); + for (SmartHomeDeviceHandler device : smartHomeDeviceHandlers) { + String id = device.getId(); + SmartHomeBaseDevice baseDevice = jsonIdSmartHomeDeviceMapping.get(id); + SmartHomeDeviceHandler.getSupportedSmartHomeDevices(baseDevice, allDevices) + .forEach(devicesToUpdate::add); + } + smartHomeDeviceStateGroupUpdateCalculator.removeDevicesWithNoUpdate(devicesToUpdate); + devicesToUpdate.stream().map(shd -> shd.applianceId).forEach(applianceId -> { + if (applianceId != null) { + applianceIds.add(applianceId); + } + }); + if (applianceIds.isEmpty()) { + return; + } + + } + Map applianceIdToCapabilityStates = connection + .getSmartHomeDeviceStatesJson(applianceIds); + + for (SmartHomeDeviceHandler smartHomeDeviceHandler : smartHomeDeviceHandlers) { + String id = smartHomeDeviceHandler.getId(); + if (requestedDeviceUpdates.contains(id)) { + logger.debug("Device update {} suspended", id); + continue; + } + if (id.equals(deviceFilterId)) { + smartHomeDeviceHandler.updateChannelStates(allDevices, applianceIdToCapabilityStates); + } + } + + logger.debug("updateSmartHomeState finished"); + } catch (HttpException | JsonSyntaxException | ConnectionException e) { + logger.debug("updateSmartHomeState fails", e); + } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail. + logger.warn("updateSmartHomeState fails with unexpected error", e); + } + } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java index eabce0939a58a..457648b257c29 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java @@ -126,7 +126,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler { private boolean updateRoutine = true; private boolean updatePlayMusicVoiceCommand = true; private boolean updateStartCommand = true; - private @Nullable Integer noticationVolumeLevel; + private @Nullable Integer notificationVolumeLevel; private @Nullable Boolean ascendingAlarm; private @Nullable JsonPlaylists playLists; private @Nullable JsonNotificationSound @Nullable [] alarmSounds; @@ -302,7 +302,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof PercentType) { int volume = ((PercentType) command).intValue(); connection.notificationVolume(device, volume); - this.noticationVolumeLevel = volume; + this.notificationVolumeLevel = volume; waitForUpdate = -1; account.forceCheckData(); } @@ -657,7 +657,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (bluetoothRefresh) { JsonBluetoothStates states; states = connection.getBluetoothConnectionStates(); - state = states.findStateByDevice(device); + if (states != null) { + state = states.findStateByDevice(device); + } } updateState(account, device, state, null, null, null, null, null); @@ -699,7 +701,7 @@ private boolean handleEqualizerCommands(String channelId, Command command, Conne newEqualizerSetting.treble = value.intValue(); } try { - connection.SetEqualizer(device, newEqualizerSetting); + connection.setEqualizer(device, newEqualizerSetting); return true; } catch (HttpException | IOException | ConnectionException e) { logger.debug("Update equalizer failed", e); @@ -765,22 +767,22 @@ private void stopCurrentNotification() { } private void updateNotificationTimerState() { - boolean stopCurrentNotifcation = true; + boolean stopCurrentNotification = true; JsonNotificationResponse currentNotification = this.currentNotification; try { if (currentNotification != null) { Connection currentConnection = this.findConnection(); if (currentConnection != null) { JsonNotificationResponse newState = currentConnection.getNotificationState(currentNotification); - if (StringUtils.equals(newState.status, "ON")) { - stopCurrentNotifcation = false; + if (newState != null && "ON".equals(newState.status)) { + stopCurrentNotification = false; } } } } catch (IOException | URISyntaxException e) { logger.warn("update notification state fails", e); } - if (stopCurrentNotifcation) { + if (stopCurrentNotification) { if (currentNotification != null) { String type = currentNotification.type; if (type != null) { @@ -807,7 +809,7 @@ public void updateState(AccountHandler accountHandler, @Nullable Device device, this.logger.debug("Handle updateState {}", this.getThing().getUID()); if (deviceNotificationState != null) { - noticationVolumeLevel = deviceNotificationState.volumeLevel; + notificationVolumeLevel = deviceNotificationState.volumeLevel; } if (ascendingAlarmModel != null) { ascendingAlarm = ascendingAlarmModel.ascendingAlarmEnabled; @@ -851,44 +853,44 @@ public void updateState(AccountHandler accountHandler, @Nullable Device device, Progress progress = null; try { JsonPlayerState playerState = connection.getPlayer(device); - playerInfo = playerState.playerInfo; - if (playerInfo != null) { - infoText = playerInfo.infoText; - if (infoText == null) { - infoText = playerInfo.miniInfoText; - } - mainArt = playerInfo.mainArt; - provider = playerInfo.provider; - if (provider != null) { - musicProviderId = provider.providerName; - // Map the music provider id to the one used for starting music with voice command - if (musicProviderId != null) { - musicProviderId = musicProviderId.toUpperCase(); - - if (StringUtils.equals(musicProviderId, "AMAZON MUSIC")) { - musicProviderId = "AMAZON_MUSIC"; - } - if (StringUtils.equals(musicProviderId, "CLOUD_PLAYER")) { - musicProviderId = "AMAZON_MUSIC"; - } - if (StringUtils.startsWith(musicProviderId, "TUNEIN")) { - musicProviderId = "TUNEIN"; - } - if (StringUtils.startsWithIgnoreCase(musicProviderId, "iHeartRadio")) { - musicProviderId = "I_HEART_RADIO"; - } - if (StringUtils.containsIgnoreCase(musicProviderId, "Apple") - && StringUtils.containsIgnoreCase(musicProviderId, "Music")) { - musicProviderId = "APPLE_MUSIC"; + if (playerState != null) { + playerInfo = playerState.playerInfo; + if (playerInfo != null) { + infoText = playerInfo.infoText; + if (infoText == null) { + infoText = playerInfo.miniInfoText; + } + mainArt = playerInfo.mainArt; + provider = playerInfo.provider; + if (provider != null) { + musicProviderId = provider.providerName; + // Map the music provider id to the one used for starting music with voice command + if (musicProviderId != null) { + musicProviderId = musicProviderId.toUpperCase(); + + if (StringUtils.equals(musicProviderId, "AMAZON MUSIC")) { + musicProviderId = "AMAZON_MUSIC"; + } + if (StringUtils.equals(musicProviderId, "CLOUD_PLAYER")) { + musicProviderId = "AMAZON_MUSIC"; + } + if (StringUtils.startsWith(musicProviderId, "TUNEIN")) { + musicProviderId = "TUNEIN"; + } + if (StringUtils.startsWithIgnoreCase(musicProviderId, "iHeartRadio")) { + musicProviderId = "I_HEART_RADIO"; + } + if (StringUtils.containsIgnoreCase(musicProviderId, "Apple") + && StringUtils.containsIgnoreCase(musicProviderId, "Music")) { + musicProviderId = "APPLE_MUSIC"; + } } } + progress = playerInfo.progress; } - progress = playerInfo.progress; } } catch (HttpException e) { - if (e.getCode() == 400) { - // Ignore - } else { + if (e.getCode() != 400) { logger.info("getPlayer fails", e); } } catch (IOException | URISyntaxException e) { @@ -896,10 +898,8 @@ public void updateState(AccountHandler accountHandler, @Nullable Device device, } // check playing isPlaying = (playerInfo != null && StringUtils.equals(playerInfo.state, "PLAYING")); - // || (mediaState != null && StringUtils.equals(mediaState.currentState, "PLAYING")); isPaused = (playerInfo != null && StringUtils.equals(playerInfo.state, "PAUSED")); - // || (mediaState != null && StringUtils.equals(mediaState.currentState, "PAUSED")); synchronized (progressLock) { Boolean showTime = null; Long mediaLength = null; @@ -947,7 +947,6 @@ public void updateState(AccountHandler accountHandler, @Nullable Device device, } // handle music provider id - if (provider != null && isPlaying) { if (musicProviderId != null) { this.musicProviderId = musicProviderId; @@ -1129,8 +1128,9 @@ public void updateState(AccountHandler accountHandler, @Nullable Device device, updateState(CHANNEL_ASCENDING_ALARM, ascendingAlarm != null ? (ascendingAlarm ? OnOffType.ON : OnOffType.OFF) : UnDefType.UNDEF); - if (noticationVolumeLevel != null) { - updateState(CHANNEL_NOTIFICATION_VOLUME, new PercentType(noticationVolumeLevel)); + final Integer notificationVolumeLevel = this.notificationVolumeLevel; + if (notificationVolumeLevel != null) { + updateState(CHANNEL_NOTIFICATION_VOLUME, new PercentType(notificationVolumeLevel)); } else { updateState(CHANNEL_NOTIFICATION_VOLUME, UnDefType.UNDEF); } @@ -1155,14 +1155,16 @@ private void updateEqualizerState() { if (device == null) { return; } - Integer bass; - Integer midrange; - Integer treble; + Integer bass = null; + Integer midrange = null; + Integer treble = null; try { JsonEqualizer equalizer = connection.getEqualizer(device); - bass = equalizer.bass; - midrange = equalizer.mid; - treble = equalizer.treble; + if (equalizer != null) { + bass = equalizer.bass; + midrange = equalizer.mid; + treble = equalizer.treble; + } this.lastKnownEqualizer = equalizer; } catch (IOException | URISyntaxException | HttpException | ConnectionException e) { logger.debug("Get equalizer failes", e); @@ -1214,7 +1216,7 @@ public void handlePushActivity(Activity pushActivity) { if ("DISCARDED_NON_DEVICE_DIRECTED_INTENT".equals(pushActivity.activityStatus)) { return; } - Description description = pushActivity.ParseDescription(); + Description description = pushActivity.parseDescription(); if (StringUtils.isEmpty(description.firstUtteranceId) || StringUtils.startsWithIgnoreCase(description.firstUtteranceId, "TextClient:")) { return; diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java new file mode 100644 index 0000000000000..ccd55db530ed6 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java @@ -0,0 +1,388 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.handler; + +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.DEVICE_PROPERTY_ID; +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.SUPPORTED_INTERFACES; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.thing.binding.BridgeHandler; +import org.eclipse.smarthome.core.thing.binding.builder.ChannelBuilder; +import org.eclipse.smarthome.core.thing.binding.builder.ThingBuilder; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.StateDescription; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroups.SmartHomeGroup; +import org.openhab.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice; +import org.openhab.binding.amazonechocontrol.internal.smarthome.Constants; +import org.openhab.binding.amazonechocontrol.internal.smarthome.HandlerBase; +import org.openhab.binding.amazonechocontrol.internal.smarthome.HandlerBase.ChannelInfo; +import org.openhab.binding.amazonechocontrol.internal.smarthome.HandlerBase.UpdateChannelResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * @author Lukas Knoeller - Initial contribution + */ +@NonNullByDefault +public class SmartHomeDeviceHandler extends BaseThingHandler { + private final Logger logger = LoggerFactory.getLogger(SmartHomeDeviceHandler.class); + + private @Nullable SmartHomeBaseDevice smartHomeBaseDevice; + private final Gson gson; + private final Map handlers = new HashMap<>(); + private final Map lastStates = new HashMap<>(); + + public SmartHomeDeviceHandler(Thing thing, Gson gson) { + super(thing); + this.gson = gson; + } + + public synchronized void setDeviceAndUpdateThingState(AccountHandler accountHandler, + @Nullable SmartHomeBaseDevice smartHomeBaseDevice) { + if (smartHomeBaseDevice == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Can't find smarthomeBaseDevice"); + return; + } + boolean changed = this.smartHomeBaseDevice == null; + this.smartHomeBaseDevice = smartHomeBaseDevice; + + Set unusedChannels = new HashSet<>(); + thing.getChannels().forEach(channel -> unusedChannels.add(channel.getUID().getId())); + + Set unusedHandlers = new HashSet<>(handlers.keySet()); + + Map> capabilities = new HashMap<>(); + getCapabilities(capabilities, accountHandler, smartHomeBaseDevice); + + ThingBuilder thingBuilder = editThing(); + + for (String interfaceName : capabilities.keySet()) { + HandlerBase handler = handlers.get(interfaceName); + if (handler != null) { + unusedHandlers.remove(interfaceName); + } else { + Supplier creator = Constants.HANDLER_FACTORY.get(interfaceName); + if (creator != null) { + handler = creator.get(); + handlers.put(interfaceName, handler); + } + } + if (handler != null) { + Collection required = handler.initialize(this, capabilities.get(interfaceName)); + for (ChannelInfo channelInfo : required) { + unusedChannels.remove(channelInfo.channelId); + if (addChannelToDevice(thingBuilder, channelInfo.channelId, channelInfo.itemType, + channelInfo.channelTypeUID)) { + changed = true; + } + } + } + } + + unusedHandlers.forEach(handlers::remove); + if (!unusedChannels.isEmpty()) { + changed = true; + unusedChannels.stream().map(id -> new ChannelUID(thing.getUID(), id)).forEach(thingBuilder::withoutChannel); + } + + if (changed) { + updateThing(thingBuilder.build()); + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Thing has changed."); + accountHandler.forceDelayedSmartHomeStateUpdate(getId()); + } + } + + public String getId() { + String id = (String) getConfig().get(DEVICE_PROPERTY_ID); + if (id == null) { + return ""; + } + return id; + } + + @Override + public void updateState(String channelId, State state) { + super.updateState(new ChannelUID(thing.getUID(), channelId), state); + } + + @Override + public void initialize() { + AccountHandler accountHandler = getAccountHandler(); + if (accountHandler != null) { + accountHandler.addSmartHomeDeviceHandler(this); + setDeviceAndUpdateThingState(accountHandler, smartHomeBaseDevice); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridgehandler not found"); + } + } + + private boolean addChannelToDevice(ThingBuilder thingBuilder, String channelId, String itemType, + ChannelTypeUID channelTypeUID) { + Channel channel = thing.getChannel(channelId); + if (channel != null) { + if (channelTypeUID.equals(channel.getChannelTypeUID()) && itemType.equals(channel.getAcceptedItemType())) { + // channel exist with the same settings + return false; + } + // channel exist with other settings, remove it first + thingBuilder.withoutChannel(channel.getUID()); + } + thingBuilder.withChannel(ChannelBuilder.create(new ChannelUID(thing.getUID(), channelId), itemType) + .withType(channelTypeUID).build()); + return true; + } + + public void updateChannelStates(List allDevices, + Map applianceIdToCapabilityStates) { + AccountHandler accountHandler = getAccountHandler(); + SmartHomeBaseDevice smartHomeBaseDevice = this.smartHomeBaseDevice; + if (smartHomeBaseDevice == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Can't find smarthomeBaseDevice!"); + return; + } + + boolean stateFound = false; + Map> mapInterfaceToStates = new HashMap<>(); + SmartHomeDevice firstDevice = null; + for (SmartHomeDevice shd : getSupportedSmartHomeDevices(smartHomeBaseDevice, allDevices)) { + JsonArray states = applianceIdToCapabilityStates.get(shd.applianceId); + String applianceId = shd.applianceId; + if (applianceId == null) { + continue; + } + if (states != null) { + stateFound = true; + if (smartHomeBaseDevice.isGroup()) { + // for groups, store the last state of all devices + lastStates.put(applianceId, states); + } + } else { + states = lastStates.get(applianceId); + if (states == null) { + continue; + } + } + if (firstDevice == null) { + firstDevice = shd; + } + for (JsonElement stateElement : states) { + String stateJson = stateElement.getAsString(); + if (stateJson.startsWith("{") && stateJson.endsWith("}")) { + JsonObject state = gson.fromJson(stateJson, JsonObject.class); + String interfaceName = state.get("namespace").getAsString(); + mapInterfaceToStates.computeIfAbsent(interfaceName, k -> new ArrayList<>()).add(state); + } + } + } + for (HandlerBase handlerBase : handlers.values()) { + if (handlerBase == null) { + continue; + } + UpdateChannelResult result = new UpdateChannelResult(); + + for (String interfaceName : handlerBase.getSupportedInterface()) { + List stateList = mapInterfaceToStates.getOrDefault(interfaceName, Collections.emptyList()); + try { + handlerBase.updateChannels(interfaceName, stateList, result); + } catch (Exception e) { + // We catch all exceptions, otherwise all other things are not updated! + logger.debug("Updating states failed", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); + } + } + + if (result.needSingleUpdate && smartHomeBaseDevice instanceof SmartHomeDevice && accountHandler != null) { + SmartHomeDevice shd = (SmartHomeDevice) smartHomeBaseDevice; + accountHandler.forceDelayedSmartHomeStateUpdate(shd.findId()); + } + } + + if (stateFound) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "State not found"); + } + } + + private @Nullable AccountHandler getAccountHandler() { + Bridge bridge = getBridge(); + if (bridge != null) { + BridgeHandler bridgeHandler = bridge.getHandler(); + if (bridgeHandler instanceof AccountHandler) { + return (AccountHandler) bridgeHandler; + } + } + + return null; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + AccountHandler accountHandler = getAccountHandler(); + if (accountHandler == null) { + logger.debug("accountHandler is null in {}", thing.getUID()); + return; + } + Connection connection = accountHandler.findConnection(); + if (connection == null) { + logger.debug("connection is null in {}", thing.getUID()); + return; + } + + try { + if (command instanceof RefreshType) { + accountHandler.forceDelayedSmartHomeStateUpdate(getId()); + return; + } + SmartHomeBaseDevice smartHomeBaseDevice = this.smartHomeBaseDevice; + if (smartHomeBaseDevice == null) { + logger.debug("smarthomeBaseDevice is null in {}", thing.getUID()); + return; + } + Set devices = getSupportedSmartHomeDevices(smartHomeBaseDevice, + accountHandler.getLastKnownSmartHomeDevices()); + String channelId = channelUID.getId(); + + for (String interfaceName : handlers.keySet()) { + HandlerBase handlerBase = handlers.get(interfaceName); + if (handlerBase == null || !handlerBase.hasChannel(channelId)) { + continue; + } + for (SmartHomeDevice shd : devices) { + String entityId = shd.entityId; + if (entityId == null) { + continue; + } + SmartHomeCapability[] capabilities = shd.capabilities; + if (capabilities == null) { + logger.debug("capabilities is null in {}", thing.getUID()); + return; + } + accountHandler.forceDelayedSmartHomeStateUpdate(getId()); // block updates + if (handlerBase.handleCommand(connection, shd, entityId, capabilities, channelUID.getId(), + command)) { + accountHandler.forceDelayedSmartHomeStateUpdate(getId()); // force update again to restart + // update timer + logger.debug("Command {} sent to {}", command, shd.findId()); + } + } + } + } catch (Exception e) { + logger.warn("Handle command failed", e); + } + } + + private static void getCapabilities(Map> result, AccountHandler accountHandler, + SmartHomeBaseDevice device) { + if (device instanceof SmartHomeDevice) { + SmartHomeDevice shd = (SmartHomeDevice) device; + SmartHomeCapability[] capabilities = shd.capabilities; + if (capabilities == null) { + return; + } + for (SmartHomeCapability capability : capabilities) { + String interfaceName = capability.interfaceName; + if (interfaceName != null) { + result.computeIfAbsent(interfaceName, name -> new ArrayList<>()).add(capability); + } + } + } + if (device instanceof SmartHomeGroup) { + for (SmartHomeDevice shd : getSupportedSmartHomeDevices(device, + accountHandler.getLastKnownSmartHomeDevices())) { + getCapabilities(result, accountHandler, shd); + } + } + } + + public static Set getSupportedSmartHomeDevices(@Nullable SmartHomeBaseDevice baseDevice, + List allDevices) { + if (baseDevice == null) { + return Collections.emptySet(); + } + Set result = new HashSet<>(); + if (baseDevice instanceof SmartHomeDevice) { + SmartHomeDevice shd = (SmartHomeDevice) baseDevice; + SmartHomeCapability[] capabilities = shd.capabilities; + if (capabilities != null) { + if (Arrays.stream(capabilities).map(capability -> capability.interfaceName) + .anyMatch(SUPPORTED_INTERFACES::contains)) { + result.add(shd); + } + } + } else { + SmartHomeGroup shg = (SmartHomeGroup) baseDevice; + for (SmartHomeBaseDevice device : allDevices) { + if (device instanceof SmartHomeDevice) { + SmartHomeDevice shd = (SmartHomeDevice) device; + if (shd.tags != null && shd.tags.tagNameToValueSetMap != null + && shd.tags.tagNameToValueSetMap.groupIdentity != null + && shg.applianceGroupIdentifier != null && shg.applianceGroupIdentifier.value != null + && Arrays.asList(shd.tags.tagNameToValueSetMap.groupIdentity) + .contains(shg.applianceGroupIdentifier.value)) { + SmartHomeCapability[] capabilities = shd.capabilities; + if (capabilities != null) { + if (Arrays.stream(capabilities).map(capability -> capability.interfaceName) + .anyMatch(SUPPORTED_INTERFACES::contains)) { + result.add(shd); + } + } + } + } + } + } + return result; + } + + public @Nullable StateDescription findStateDescription(Channel channel, StateDescription originalStateDescription, + @Nullable Locale locale) { + String channelId = channel.getUID().getId(); + for (HandlerBase handler : handlers.values()) { + if (handler != null && handler.hasChannel(channelId)) { + return handler.findStateDescription(channelId, originalStateDescription, locale); + } + } + return null; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonActivities.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonActivities.java index 321ab282bd21d..499e4617b474c 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonActivities.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonActivities.java @@ -12,7 +12,6 @@ */ package org.openhab.binding.amazonechocontrol.internal.jsons; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -20,7 +19,7 @@ import com.google.gson.JsonSyntaxException; /** - * The {@link JsonActivity} encapsulate the GSON data of the push command for push activity + * The {@link JsonActivities} encapsulate the GSON data of the push command for push activity * * @author Michael Geramb - Initial contribution */ @@ -58,14 +57,16 @@ public static class Description { public @Nullable String firstStreamId; } - public Description ParseDescription() { + public Description parseDescription() { String description = this.description; - if (StringUtils.isEmpty(description) || !description.startsWith("{") || !description.endsWith("}")) { + if (description == null || description.isEmpty() || !description.startsWith("{") + || !description.endsWith("}")) { return new Description(); } Gson gson = new Gson(); try { - return gson.fromJson(description, Description.class); + Description description1 = gson.fromJson(description, Description.class); + return description1 != null ? description1 : new Description(); } catch (JsonSyntaxException e) { return new Description(); } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAnnouncementTarget.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAnnouncementTarget.java index 5612805da66b8..29c5393147dcc 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAnnouncementTarget.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAnnouncementTarget.java @@ -16,7 +16,8 @@ import org.eclipse.jdt.annotation.Nullable; /** - * The {@link JsonActivity} encapsulate the GSON data of the sequence command AlexaAnnouncement for announcement target + * The {@link JsonAnnouncementTarget} encapsulate the GSON data of the sequence command AlexaAnnouncement for + * announcement target * * @author Michael Geramb - Initial contribution */ diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAscendingAlarm.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAscendingAlarm.java index 444a2ec67c461..6f4015e22db46 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAscendingAlarm.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAscendingAlarm.java @@ -16,7 +16,7 @@ import org.eclipse.jdt.annotation.Nullable; /** - * The {@link JsonActivity} encapsulate the GSON data of the /api/ascending-alarm response + * The {@link JsonAscendingAlarm} encapsulate the GSON data of the /api/ascending-alarm response * * @author Michael Geramb - Initial contribution */ diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonColorTemperature.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonColorTemperature.java new file mode 100644 index 0000000000000..f40d2edafd971 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonColorTemperature.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * @author Lukas Knoeller - Initial contribution + */ +@NonNullByDefault +public class JsonColorTemperature { + public @Nullable String temperatureName; + + public JsonColorTemperature(String temperature) { + temperatureName = temperature; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonColors.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonColors.java new file mode 100644 index 0000000000000..65a79cc89d74e --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonColors.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * @author Lukas Knoeller - Initial contribution + */ +@NonNullByDefault +public class JsonColors { + public @Nullable String colorName; + + public JsonColors(String colorName) { + this.colorName = colorName; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushActivity.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushActivity.java index e62fcdf02b7f0..cff02a45403da 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushActivity.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushActivity.java @@ -16,7 +16,7 @@ import org.eclipse.jdt.annotation.Nullable; /** - * The {@link JsonPushPayloadCommand} encapsulate the GSON data of the push command with device information + * The {@link JsonCommandPayloadPushActivity} encapsulate the GSON data of the push command with device information * * @author Michael Geramb - Initial contribution */ diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushDevice.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushDevice.java index 95fee29973f9a..4abcd34454d07 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushDevice.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushDevice.java @@ -16,7 +16,7 @@ import org.eclipse.jdt.annotation.Nullable; /** - * The {@link JsonPushPayloadCommand} encapsulate the GSON data of the push command with device information + * The {@link JsonCommandPayloadPushDevice} encapsulate the GSON data of the push command with device information * * @author Michael Geramb - Initial contribution */ diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDeviceNotificationState.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDeviceNotificationState.java index 1d5093860c7b0..ee1cf48f0a275 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDeviceNotificationState.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDeviceNotificationState.java @@ -16,7 +16,7 @@ import org.eclipse.jdt.annotation.Nullable; /** - * The {@link JsonActivity} encapsulate the GSON data of the /api/device-notification-state response + * The {@link JsonDeviceNotificationState} encapsulate the GSON data of the /api/device-notification-state response * * @author Michael Geramb - Initial contribution */ diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonExchangeTokenResponse.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonExchangeTokenResponse.java index dc357c3cac3c7..627f01b79eed4 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonExchangeTokenResponse.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonExchangeTokenResponse.java @@ -17,6 +17,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import com.google.gson.annotations.SerializedName; + /** * The {@link JsonExchangeTokenResponse} encapsulate the GSON response data of the token exchange * @@ -35,11 +37,17 @@ public static class Tokens { } public static class Cookie { - public @Nullable String Path; - public @Nullable Boolean Secure; - public @Nullable String Value; - public @Nullable String Expires; - public @Nullable Boolean HttpOnly; - public @Nullable String Name; + @SerializedName("Path") + public @Nullable String path; + @SerializedName("Secure") + public @Nullable Boolean secure; + @SerializedName("Value") + public @Nullable String value; + @SerializedName("Expires") + public @Nullable String expires; + @SerializedName("HttpOnly") + public @Nullable Boolean httpOnly; + @SerializedName("Name") + public @Nullable String name; } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java new file mode 100644 index 0000000000000..34617fd42c637 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link JsonNetworkDetails} encapsulate the GSON data of a network query + * + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class JsonNetworkDetails { + public @Nullable String networkDetail; +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRegisterAppRequest.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRegisterAppRequest.java index 878ce532d9a3d..5e3a7d629c730 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRegisterAppRequest.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRegisterAppRequest.java @@ -15,52 +15,65 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import com.google.gson.annotations.SerializedName; + /** - * The {@link JsonRegisterApp} encapsulate the GSON data of register application request + * The {@link JsonRegisterAppRequest} encapsulate the GSON data of register application request * * @author Michael Geramb - Initial contribution */ @NonNullByDefault public class JsonRegisterAppRequest { - public JsonRegisterAppRequest(String serial, String access_token, String frc, JsonWebSiteCookie[] webSiteCookies) { - registration_data.device_serial = serial; - auth_data.access_token = access_token; - user_context_map.frc = frc; - cookies.website_cookies = webSiteCookies; + public JsonRegisterAppRequest(String serial, String accessToken, String frc, JsonWebSiteCookie[] webSiteCookies) { + registrationData.deviceSerial = serial; + authData.accessToken = accessToken; + userContextMap.frc = frc; + cookies.webSiteCookies = webSiteCookies; } - public String[] requested_extensions = { "device_info", "customer_info" }; + @SerializedName("requested_extensions") + public String[] requestedExtensions = { "device_info", "customer_info" }; public Cookies cookies = new Cookies(); - public RegistrationData registration_data = new RegistrationData(); - public AuthData auth_data = new AuthData(); - public UserContextMap user_context_map = new UserContextMap(); - public String[] requested_token_type = { "bearer", "mac_dms", "website_cookies" }; + @SerializedName("registration_data") + public RegistrationData registrationData = new RegistrationData(); + @SerializedName("auth_data") + public AuthData authData = new AuthData(); + @SerializedName("user_context_map") + public UserContextMap userContextMap = new UserContextMap(); + @SerializedName("requested_token_type") + public String[] requestedTokenType = { "bearer", "mac_dms", "website_cookies" }; public static class Cookies { - @Nullable - public JsonWebSiteCookie @Nullable [] website_cookies; - @Nullable - public String domain = ".amazon.com"; + @SerializedName("website_cookies") + public @Nullable JsonWebSiteCookie @Nullable [] webSiteCookies; + public @Nullable String domain = ".amazon.com"; } public static class RegistrationData { public String domain = "Device"; - public String app_version = "2.2.223830.0"; - public String device_type = "A2IVLV5VM2W81"; - public String device_name = "%FIRST_NAME%'s%DUPE_STRATEGY_1ST%Open HAB Alexa Binding"; - public String os_version = "11.4.1"; - @Nullable - public String device_serial; - public String device_model = "iPhone"; - public String app_name = "Open HAB Alexa Binding";// Amazon Alexa"; - public String software_version = "1"; + @SerializedName("app_version") + public String appVersion = "2.2.223830.0"; + @SerializedName("device_type") + public String deviceType = "A2IVLV5VM2W81"; + @SerializedName("device_name") + public String deviceName = "%FIRST_NAME%'s%DUPE_STRATEGY_1ST%openHAB Alexa Binding"; + @SerializedName("os_version") + public String osVersion = "11.4.1"; + @SerializedName("device_serial") + public @Nullable String deviceSerial; + @SerializedName("device_model") + public String deviceModel = "iPhone"; + @SerializedName("app_name") + public String appName = "openHAB Alexa Binding"; + @SerializedName("software_version") + public String softwareVersion = "1"; } public static class AuthData { - @Nullable - public String access_token; + @SerializedName("access_token") + public @Nullable String accessToken; } public static class UserContextMap { diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRegisterAppResponse.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRegisterAppResponse.java index 277e815d03b30..de7e1569b4a94 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRegisterAppResponse.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRegisterAppResponse.java @@ -15,6 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import com.google.gson.annotations.SerializedName; + /** * The {@link JsonRegisterAppResponse} encapsulate the GSON data of response from the register command * @@ -23,79 +25,88 @@ @NonNullByDefault public class JsonRegisterAppResponse { - @Nullable - public Response response; + public @Nullable Response response; - @Nullable - public String request_id; + @SerializedName("request_id") + public @Nullable String requestId; public static class Response { - @Nullable - public Success success; + public @Nullable Success success; } public static class Success { - @Nullable - public Extensions extensions; - @Nullable - public Tokens tokens; - @Nullable - public String customer_id; + public @Nullable Extensions extensions; + + public @Nullable Tokens tokens; + + @SerializedName("customer_id") + public @Nullable String customerId; } public static class Extensions { - @Nullable - public DeviceInfo device_info; - @Nullable - public CustomerInfo customer_info; - @Nullable - public String customer_id; + @SerializedName("device_info") + public @Nullable DeviceInfo deviceInfo; + + @SerializedName("customer_info") + public @Nullable CustomerInfo customerInfo; + + @SerializedName("customer_id") + public @Nullable String customerId; } public static class DeviceInfo { - @Nullable - public String device_name; - @Nullable - public String device_serial_number; - @Nullable - public String device_type; + @SerializedName("device_name") + public @Nullable String deviceName; + + @SerializedName("device_serial_number") + public @Nullable String deviceSerialNumber; + + @SerializedName("device_type") + public @Nullable String deviceType; } public static class CustomerInfo { - @Nullable - public String account_pool; - @Nullable - public String user_id; - @Nullable - public String home_region; - @Nullable - public String name; - @Nullable - public String given_name; + @SerializedName("account_pool") + public @Nullable String accountPool; + + @SerializedName("user_id") + public @Nullable String userId; + + @SerializedName("home_region") + public @Nullable String homeRegion; + + public @Nullable String name; + + @SerializedName("given_name") + public @Nullable String givenName; } public static class Tokens { - @Nullable - public Object website_cookies; - @Nullable - public MacDms mac_dms; - @Nullable - public Bearer bearer; + @SerializedName("website_cookies") + public @Nullable Object websiteCookies; + + @SerializedName("mac_dms") + public @Nullable MacDms macDms; + + public @Nullable Bearer bearer; } public static class MacDms { - @Nullable - public String device_private_key; - @Nullable - public String adp_token; + @SerializedName("device_private_key") + public @Nullable String devicePrivateKey; + + @SerializedName("adp_token") + public @Nullable String adpToken; } public static class Bearer { - @Nullable - public String access_token; - @Nullable - public String refresh_token; - @Nullable - public String expires_in; + @SerializedName("access_token") + public @Nullable String accessToken; + + @SerializedName("refresh_token") + public @Nullable String refreshToken; + + @SerializedName("expires_in") + public @Nullable String expiresIn; } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRenewTokenResponse.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRenewTokenResponse.java index 7133fb8451963..659b4089b4ef3 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRenewTokenResponse.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRenewTokenResponse.java @@ -15,6 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import com.google.gson.annotations.SerializedName; + /** * The {@link JsonRenewTokenResponse} encapsulate the GSON response of the renew token request * @@ -22,7 +24,10 @@ */ @NonNullByDefault public class JsonRenewTokenResponse { - public @Nullable String access_token; - public @Nullable String token_type; - public @Nullable Long expires_in; + @SerializedName("access_token") + public @Nullable String accessToken; + @SerializedName("token_type") + public @Nullable String tokenType; + @SerializedName("expires_in") + public @Nullable Long expiresIn; } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeCapabilities.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeCapabilities.java new file mode 100644 index 0000000000000..e136c1abe2bfd --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeCapabilities.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * @author Lukas Knoeller - Initial contribution + */ +@NonNullByDefault +public class JsonSmartHomeCapabilities { + + public static class SmartHomeCapability { + public @Nullable String capabilityType; + public @Nullable String type; + public @Nullable String version; + public @Nullable String interfaceName; + public @Nullable Properties properties; + } + + public static class Properties { + public @Nullable Property @Nullable [] supported; + } + + public static class Property { + public @Nullable String name; + } + + public @Nullable SmartHomeCapability @Nullable [] capabilites; +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDeviceAlias.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDeviceAlias.java new file mode 100644 index 0000000000000..46d7e42cd7fc6 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDeviceAlias.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * @author Lukas Knoeller - Initial contribution + */ +@NonNullByDefault +public class JsonSmartHomeDeviceAlias { + public @Nullable String friendlyName; + public @Nullable Boolean enabled; + + public JsonSmartHomeDeviceAlias(String friendlyName, Boolean enabled) { + this.friendlyName = friendlyName; + this.enabled = enabled; + } + + public JsonSmartHomeDeviceAlias() { + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDeviceNetworkState.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDeviceNetworkState.java new file mode 100644 index 0000000000000..0d66349f393b7 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDeviceNetworkState.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * + * @author Lukas Knoeller - Initial contribution + * + */ +@NonNullByDefault +public class JsonSmartHomeDeviceNetworkState { + public static class SmartHomeDeviceNetworkState { + public @Nullable String reachability; + } + + public @Nullable SmartHomeDeviceNetworkState networkState; +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevices.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevices.java new file mode 100644 index 0000000000000..ea6584b7bbccf --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevices.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDeviceNetworkState.SmartHomeDeviceNetworkState; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeTags.JsonSmartHomeTag; + +/** + * @author Lukas Knoeller - Initial contribution + */ +@NonNullByDefault +public class JsonSmartHomeDevices { + public static class SmartHomeDevice implements SmartHomeBaseDevice { + + public @Nullable Integer updateIntervalInSeconds; + + @Override + public @Nullable String findId() { + return applianceId; + } + + @Override + public boolean isGroup() { + return false; + } + + public @Nullable String applianceId; + public @Nullable String manufacturerName; + public @Nullable String friendlyDescription; + public @Nullable String modelName; + public @Nullable String friendlyName; + public @Nullable String reachability; + public @Nullable String entityId; + public @Nullable SmartHomeDeviceNetworkState applianceNetworkState; + public @Nullable SmartHomeCapability @Nullable [] capabilities; + public @Nullable JsonSmartHomeTag tags; + public @Nullable String @Nullable [] applianceTypes; + public @Nullable JsonSmartHomeDeviceAlias @Nullable [] aliases; + public @Nullable SmartHomeDevice @Nullable [] groupDevices; + public @Nullable String connectedVia; + public @Nullable DriverIdentity driverIdentity; + } + + public static class DriverIdentity { + public @Nullable String namespace; + public @Nullable String identifier; + } + + public @Nullable SmartHomeDevice @Nullable [] smarthomeDevices; +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroupIdentifiers.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroupIdentifiers.java new file mode 100644 index 0000000000000..ce92ea8009558 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroupIdentifiers.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * @author Lukas Knoeller - Initial contribution + */ +@NonNullByDefault +public class JsonSmartHomeGroupIdentifiers { + + public static class SmartHomeGroupIdentifier { + public @Nullable String value; + } + + public @Nullable SmartHomeGroupIdentifier identifier; +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroupIdentity.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroupIdentity.java new file mode 100644 index 0000000000000..06deba378bf7b --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroupIdentity.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * + * @author Lukas Knoeller - Initial contribution + * + */ +@NonNullByDefault +public class JsonSmartHomeGroupIdentity { + public static class SmartHomeGroupIdentity { + public @Nullable String @Nullable [] groupIdentity; + } + + public @Nullable SmartHomeGroupIdentity @Nullable [] groupIdentity; +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroups.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroups.java new file mode 100644 index 0000000000000..4281534ee84dd --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroups.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroupIdentifiers.SmartHomeGroupIdentifier; + +/** + * @author Lukas Knoeller - Initial contribution + */ +@NonNullByDefault +public class JsonSmartHomeGroups { + + public static class SmartHomeGroup implements SmartHomeBaseDevice { + + @Override + public @Nullable String findId() { + SmartHomeGroupIdentifier applianceGroupIdentifier = this.applianceGroupIdentifier; + if (applianceGroupIdentifier == null) { + return null; + } + String value = applianceGroupIdentifier.value; + if (value == null) { + return null; + } + return value; + } + + @Override + public boolean isGroup() { + return true; + } + + public @Nullable String applianceGroupName; + public @Nullable Boolean isSpace; + public @Nullable Boolean space; + public @Nullable SmartHomeGroupIdentifier applianceGroupIdentifier; + } + + public @Nullable SmartHomeGroup @Nullable [] groups; +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeTags.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeTags.java new file mode 100644 index 0000000000000..6873c2cbd9bbc --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeTags.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroupIdentity.SmartHomeGroupIdentity; + +/** + * @author Lukas Knoeller - Initial contribution + */ +@NonNullByDefault +public class JsonSmartHomeTags { + public static class JsonSmartHomeTag { + public @Nullable SmartHomeGroupIdentity tagNameToValueSetMap; + } + + public @Nullable SmartHomeGroupIdentity tagNameToValueSetMap; +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonTokenResponse.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonTokenResponse.java index e95ced344848f..cda9d0380fe14 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonTokenResponse.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonTokenResponse.java @@ -15,18 +15,19 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import com.google.gson.annotations.SerializedName; + /** - * The {@link JsonActivity} encapsulate the GSON data of the token response + * The {@link JsonTokenResponse} encapsulate the GSON data of the token response * * @author Michael Geramb - Initial contribution */ @NonNullByDefault public class JsonTokenResponse { - - @Nullable - public String access_token; - @Nullable - public String token_type; - @Nullable - public Integer expires_in; + @SerializedName("access_token") + public @Nullable String accessToken; + @SerializedName("token_type") + public @Nullable String tokenType; + @SerializedName("expires_in") + public @Nullable Integer expiresIn; } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonUsersMeResponse.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonUsersMeResponse.java index 87b283a3fee68..58a2e758c64dd 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonUsersMeResponse.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonUsersMeResponse.java @@ -16,32 +16,21 @@ import org.eclipse.jdt.annotation.Nullable; /** - * The {@link JsonActivity} encapsulate the GSON data of the users me response + * The {@link JsonUsersMeResponse} encapsulate the GSON data of the users me response * * @author Michael Geramb - Initial contribution */ @NonNullByDefault public class JsonUsersMeResponse { - @Nullable - public String countryOfResidence; - @Nullable - public String effectiveMarketPlaceId; - @Nullable - public String email; - @Nullable - public Boolean eulaAcceptance; - @Nullable - public String @Nullable [] features; - @Nullable - public String fullName; - @Nullable - public Boolean hasActiveDopplers; - @Nullable - public String id; - @Nullable - public String marketPlaceDomainName; - @Nullable - public String marketPlaceId; - @Nullable - public String marketPlaceLocale; + public @Nullable String countryOfResidence; + public @Nullable String effectiveMarketPlaceId; + public @Nullable String email; + public @Nullable Boolean eulaAcceptance; + public @Nullable String @Nullable [] features; + public @Nullable String fullName; + public @Nullable Boolean hasActiveDopplers; + public @Nullable String id; + public @Nullable String marketPlaceDomainName; + public @Nullable String marketPlaceId; + public @Nullable String marketPlaceLocale; } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonWebSiteCookie.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonWebSiteCookie.java index 948b01711983b..f35975530be51 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonWebSiteCookie.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonWebSiteCookie.java @@ -15,6 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import com.google.gson.annotations.SerializedName; + /** * The {@link JsonWebSiteCookie} encapsulate the GSON data of register cookie array * @@ -23,12 +25,12 @@ @NonNullByDefault public class JsonWebSiteCookie { public JsonWebSiteCookie(String name, String value) { - Name = name; - Value = value; + this.name = name; + this.value = value; } - @Nullable - public String Value; - @Nullable - public String Name; + @SerializedName("Value") + public @Nullable String value; + @SerializedName("Name") + public @Nullable String name; } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/SmartHomeBaseDevice.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/SmartHomeBaseDevice.java new file mode 100644 index 0000000000000..14cab708f502b --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/SmartHomeBaseDevice.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link SmartHomeBaseDevice} is the base interface for all smart home device json nodes + * + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public interface SmartHomeBaseDevice { + @Nullable + String findId(); + + boolean isGroup(); +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/Constants.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/Constants.java new file mode 100644 index 0000000000000..f6d8e393fe0b9 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/Constants.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; + +/** + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class Constants { + public static final Map> HANDLER_FACTORY = new HashMap<>(); + + static { + HANDLER_FACTORY.put(HandlerPowerController.INTERFACE, HandlerPowerController::new); + HANDLER_FACTORY.put(HandlerBrightnessController.INTERFACE, HandlerBrightnessController::new); + HANDLER_FACTORY.put(HandlerColorController.INTERFACE, HandlerColorController::new); + HANDLER_FACTORY.put(HandlerColorTemperatureController.INTERFACE, HandlerColorTemperatureController::new); + HANDLER_FACTORY.put(HandlerSecurityPanelController.INTERFACE, HandlerSecurityPanelController::new); + HANDLER_FACTORY.put(HandlerAcousticEventSensor.INTERFACE, HandlerAcousticEventSensor::new); + HANDLER_FACTORY.put(HandlerTemperatureSensor.INTERFACE, HandlerTemperatureSensor::new); + HANDLER_FACTORY.put(HandlerPercentageController.INTERFACE, HandlerPercentageController::new); + HANDLER_FACTORY.put(HandlerPowerLevelController.INTERFACE, HandlerPowerLevelController::new); + } + + public static final Set SUPPORTED_INTERFACES = HANDLER_FACTORY.keySet(); + + // channel types + public static final ChannelTypeUID CHANNEL_TYPE_TEMPERATURE = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "temperature"); + + // List of Item types + public static final String ITEM_TYPE_SWITCH = "Switch"; + public static final String ITEM_TYPE_DIMMER = "Dimmer"; + public static final String ITEM_TYPE_STRING = "String"; + public static final String ITEM_TYPE_NUMBER = "Number"; + public static final String ITEM_TYPE_NUMBER_TEMPERATURE = "Number:Temperature"; + public static final String ITEM_TYPE_CONTACT = "Contact"; + public static final String ITEM_TYPE_COLOR = "Color"; +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/DynamicStateDescriptionSmartHome.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/DynamicStateDescriptionSmartHome.java new file mode 100644 index 0000000000000..e1916119e87a7 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/DynamicStateDescriptionSmartHome.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.BINDING_ID; + +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingRegistry; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.thing.type.DynamicStateDescriptionProvider; +import org.eclipse.smarthome.core.types.StateDescription; +import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * + * Dynamic channel state description provider + * Overrides the state description for the colors of the smart bulbs + * + * @author Lukas Knoeller - Initial contribution + * + */ + +@Component(service = { DynamicStateDescriptionProvider.class, DynamicStateDescriptionSmartHome.class }) +@NonNullByDefault +public class DynamicStateDescriptionSmartHome implements DynamicStateDescriptionProvider { + private final ThingRegistry thingRegistry; + + @Activate + public DynamicStateDescriptionSmartHome(@Reference ThingRegistry thingRegistry) { + this.thingRegistry = thingRegistry; + } + + public @Nullable SmartHomeDeviceHandler findHandler(Channel channel) { + Thing thing = thingRegistry.get(channel.getUID().getThingUID()); + if (thing == null) { + return null; + } + ThingHandler handler = thing.getHandler(); + if (!(handler instanceof SmartHomeDeviceHandler)) { + return null; + } + SmartHomeDeviceHandler smartHomeHandler = (SmartHomeDeviceHandler) handler; + return smartHomeHandler; + } + + @Override + public @Nullable StateDescription getStateDescription(Channel channel, + @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { + ChannelTypeUID channelTypeUID = channel.getChannelTypeUID(); + if (channelTypeUID != null || !BINDING_ID.equals(channelTypeUID.getBindingId())) { + return null; + } + if (originalStateDescription == null) { + return null; + } + SmartHomeDeviceHandler handler = findHandler(channel); + if (handler != null) { + return handler.findStateDescription(channel, originalStateDescription, locale); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerAcousticEventSensor.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerAcousticEventSensor.java new file mode 100644 index 0000000000000..3c4b7bd70828e --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerAcousticEventSensor.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_CONTACT; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.OpenClosedType; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.StateDescription; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; + +import com.google.gson.JsonObject; + +/** + * The {@link HandlerAcousticEventSensor} is responsible for the Alexa.PowerControllerInterface + * + * @author Lukas Knoeller - Initial contribution + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class HandlerAcousticEventSensor extends HandlerBase { + // Interface + public static final String INTERFACE = "Alexa.AcousticEventSensor"; + + // Channel types + private static final ChannelTypeUID CHANNEL_TYPE_GLASS_BREAK_DETECTION_STATE = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "glassBreakDetectionState"); + private static final ChannelTypeUID CHANNEL_TYPE_SMOKE_ALARM_DETECTION_STATE = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "smokeAlarmDetectionState"); + + // Channel definitions + private static final ChannelInfo GLASS_BREAK_DETECTION_STATE = new ChannelInfo( + "glassBreakDetectionState" /* propertyName */ , "glassBreakDetectionState" /* ChannelId */, + CHANNEL_TYPE_GLASS_BREAK_DETECTION_STATE /* Channel Type */ , ITEM_TYPE_CONTACT /* Item Type */); + private static final ChannelInfo SMOKE_ALARM_DETECTION_STATE = new ChannelInfo( + "smokeAlarmDetectionState" /* propertyName */ , "smokeAlarmDetectionState" /* ChannelId */, + CHANNEL_TYPE_SMOKE_ALARM_DETECTION_STATE /* Channel Type */ , ITEM_TYPE_CONTACT /* Item Type */); + + private ChannelInfo[] getAlarmChannels() { + return new ChannelInfo[] { GLASS_BREAK_DETECTION_STATE, SMOKE_ALARM_DETECTION_STATE }; + } + + @Override + public String[] getSupportedInterface() { + return new String[] { INTERFACE }; + } + + @Override + protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + for (ChannelInfo channelInfo : getAlarmChannels()) { + if (channelInfo.propertyName.equals(property)) { + return new ChannelInfo[] { channelInfo }; + } + } + return null; + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + Boolean glassBreakDetectionStateValue = null; + Boolean smokeAlarmDetectionStateValue = null; + for (JsonObject state : stateList) { + if (GLASS_BREAK_DETECTION_STATE.propertyName.equals(state.get("name").getAsString())) { + if (glassBreakDetectionStateValue == null) { + glassBreakDetectionStateValue = !"NOT_DETECTED" + .equals(state.get("value").getAsJsonObject().get("value").getAsString()); + } + } else if (SMOKE_ALARM_DETECTION_STATE.propertyName.equals(state.get("name").getAsString())) { + if (smokeAlarmDetectionStateValue == null) { + smokeAlarmDetectionStateValue = !"NOT_DETECTED" + .equals(state.get("value").getAsJsonObject().get("value").getAsString()); + } + } + } + updateState(GLASS_BREAK_DETECTION_STATE.channelId, glassBreakDetectionStateValue == null ? UnDefType.UNDEF + : (glassBreakDetectionStateValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); + updateState(SMOKE_ALARM_DETECTION_STATE.channelId, smokeAlarmDetectionStateValue == null ? UnDefType.UNDEF + : (smokeAlarmDetectionStateValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); + } + + @Override + public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, + SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException { + return false; + } + + @Override + public @Nullable StateDescription findStateDescription(String channelUID, StateDescription originalStateDescription, + @Nullable Locale locale) { + return null; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBase.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBase.java new file mode 100644 index 0000000000000..8def553715988 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBase.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.StateDescription; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.Properties; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.Property; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; + +import com.google.gson.JsonObject; + +/** + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public abstract class HandlerBase { + protected @Nullable SmartHomeDeviceHandler smartHomeDeviceHandler; + protected Map channels = new HashMap<>(); + + protected abstract ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property); + + public abstract void updateChannels(String interfaceName, List stateList, UpdateChannelResult result); + + public abstract boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, + SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException; + + public abstract @Nullable StateDescription findStateDescription(String channelId, + StateDescription originalStateDescription, @Nullable Locale locale); + + public boolean hasChannel(String channelId) { + return channels.containsKey(channelId); + } + + public abstract String[] getSupportedInterface(); + + SmartHomeDeviceHandler getSmartHomeDeviceHandler() throws IllegalStateException { + SmartHomeDeviceHandler smartHomeDeviceHandler = this.smartHomeDeviceHandler; + if (smartHomeDeviceHandler == null) { + throw new IllegalStateException("Handler not initialized"); + } + return smartHomeDeviceHandler; + } + + public Collection initialize(SmartHomeDeviceHandler smartHomeDeviceHandler, + List capabilities) { + this.smartHomeDeviceHandler = smartHomeDeviceHandler; + Map channels = new HashMap<>(); + for (SmartHomeCapability capability : capabilities) { + Properties properties = capability.properties; + if (properties != null) { + Property @Nullable [] supported = properties.supported; + if (supported != null) { + for (Property property : supported) { + if (property != null) { + String name = property.name; + if (name != null) { + ChannelInfo[] channelInfos = findChannelInfos(capability, name); + if (channelInfos != null) { + for (ChannelInfo channelInfo : channelInfos) { + if (channelInfo != null) { + channels.put(channelInfo.channelId, channelInfo); + } + } + } + } + } + } + } + } + } + this.channels = channels; + return channels.values(); + } + + protected boolean containsCapabilityProperty(SmartHomeCapability[] capabilties, String propertyName) { + for (SmartHomeCapability capability : capabilties) { + Properties properties = capability.properties; + if (properties != null) { + Property @Nullable [] supportedProperties = properties.supported; + if (supportedProperties != null) { + for (Property property : supportedProperties) { + if (property != null) { + if (propertyName != null && propertyName.equals(property.name)) { + return true; + } + } + } + } + } + } + return false; + } + + public void updateState(String channelId, State state) { + getSmartHomeDeviceHandler().updateState(channelId, state); + } + + public static class ChannelInfo { + public final String propertyName; + public final String channelId; + public final String itemType; + public ChannelTypeUID channelTypeUID; + + public ChannelInfo(String propertyName, String channelId, ChannelTypeUID channelTypeUID, String itemType) { + this.propertyName = propertyName; + this.channelId = channelId; + this.itemType = itemType; + this.channelTypeUID = channelTypeUID; + } + } + + public static class UpdateChannelResult { + public boolean needSingleUpdate; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBrightnessController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBrightnessController.java new file mode 100644 index 0000000000000..7cf92fd8b9309 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBrightnessController.java @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_DIMMER; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.StateDescription; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; + +import com.google.gson.JsonObject; + +/** + * The {@link HandlerBrightnessController} is responsible for the Alexa.PowerControllerInterface + * + * @author Lukas Knoeller - Initial contribution + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class HandlerBrightnessController extends HandlerBase { + // Interface + public static final String INTERFACE = "Alexa.BrightnessController"; + + // Channel types + private static final ChannelTypeUID CHANNEL_TYPE_BRIGHTNESS = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "brightness"); + + // Channel definitions + private static final ChannelInfo BRIGHTNESS = new ChannelInfo("brightness" /* propertyName */ , + "brightness" /* ChannelId */, CHANNEL_TYPE_BRIGHTNESS /* Channel Type */ , + ITEM_TYPE_DIMMER /* Item Type */); + + private @Nullable Integer lastBrightness; + + @Override + public String[] getSupportedInterface() { + return new String[] { INTERFACE }; + } + + @Override + protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + if (BRIGHTNESS.propertyName.equals(property)) { + return new ChannelInfo[] { BRIGHTNESS }; + } + return null; + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + Integer brightnessValue = null; + for (JsonObject state : stateList) { + if (BRIGHTNESS.propertyName.equals(state.get("name").getAsString())) { + int value = state.get("value").getAsInt(); + // For groups take the maximum + if (brightnessValue == null) { + brightnessValue = value; + } else if (value > brightnessValue) { + brightnessValue = value; + } + } + } + if (brightnessValue != null) { + lastBrightness = brightnessValue; + } + updateState(BRIGHTNESS.channelId, brightnessValue == null ? UnDefType.UNDEF : new PercentType(brightnessValue)); + } + + @Override + public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, + SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException { + if (channelId.equals(BRIGHTNESS.channelId)) { + if (containsCapabilityProperty(capabilties, BRIGHTNESS.propertyName)) { + if (command.equals(IncreaseDecreaseType.INCREASE)) { + Integer lastBrightness = this.lastBrightness; + if (lastBrightness != null) { + int newValue = lastBrightness++; + if (newValue > 100) { + newValue = 100; + } + this.lastBrightness = newValue; + connection.smartHomeCommand(entityId, "setBrightness", BRIGHTNESS.propertyName, newValue); + return true; + } + } else if (command.equals(IncreaseDecreaseType.DECREASE)) { + Integer lastBrightness = this.lastBrightness; + if (lastBrightness != null) { + int newValue = lastBrightness--; + if (newValue < 0) { + newValue = 0; + } + this.lastBrightness = newValue; + connection.smartHomeCommand(entityId, "setBrightness", BRIGHTNESS.propertyName, newValue); + return true; + } + } else if (command.equals(OnOffType.OFF)) { + lastBrightness = 0; + connection.smartHomeCommand(entityId, "setBrightness", BRIGHTNESS.propertyName, 0); + return true; + } else if (command.equals(OnOffType.ON)) { + lastBrightness = 100; + connection.smartHomeCommand(entityId, "setBrightness", BRIGHTNESS.propertyName, 100); + return true; + } else if (command instanceof PercentType) { + lastBrightness = ((PercentType) command).intValue(); + connection.smartHomeCommand(entityId, "setBrightness", BRIGHTNESS.propertyName, + ((PercentType) command).floatValue() / 100); + return true; + } + } + } + return false; + } + + @Override + public @Nullable StateDescription findStateDescription(String channelId, StateDescription originalStateDescription, + @Nullable Locale locale) { + return null; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java new file mode 100644 index 0000000000000..561f34105859a --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_COLOR; +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_STRING; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.HSBType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.StateDescription; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; + +import com.google.gson.JsonObject; + +/** + * The {@link HandlerColorController} is responsible for the Alexa.ColorTemperatureController + * + * @author Lukas Knoeller - Initial contribution + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class HandlerColorController extends HandlerBase { + // Interface + public static final String INTERFACE = "Alexa.ColorController"; + public static final String INTERFACE_COLOR_PROPERTIES = "Alexa.ColorPropertiesController"; + + // Channel types + private static final ChannelTypeUID CHANNEL_TYPE_COLOR_NAME = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "colorName"); + + private static final ChannelTypeUID CHANNEL_TYPE_COLOR = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "color"); + + // Channel and Properties + private static final ChannelInfo COLOR = new ChannelInfo("color" /* propertyName */, "color" /* ChannelId */, + CHANNEL_TYPE_COLOR /* Channel Type */, ITEM_TYPE_COLOR /* Item Type */); + + private static final ChannelInfo COLOR_PROPERTIES = new ChannelInfo("colorProperties" /* propertyName */, + "colorName" /* ChannelId */, CHANNEL_TYPE_COLOR_NAME /* Channel Type */, ITEM_TYPE_STRING /* Item Type */); + + private @Nullable HSBType lastColor; + private @Nullable String lastColorName; + + @Override + public String[] getSupportedInterface() { + return new String[] { INTERFACE, INTERFACE_COLOR_PROPERTIES }; + } + + @Override + protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + if (COLOR.propertyName.contentEquals(property)) { + return new ChannelInfo[] { COLOR, COLOR_PROPERTIES }; + } + return null; + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + if (INTERFACE.equals(interfaceName)) { + // WRITING TO THIS CHANNEL DOES CURRENTLY NOT WORK, BUT WE LEAVE THE CODE FOR FUTURE USE! + HSBType colorValue = null; + for (JsonObject state : stateList) { + if (COLOR.propertyName.equals(state.get("name").getAsString())) { + JsonObject value = state.get("value").getAsJsonObject(); + // For groups take the maximum + if (colorValue == null) { + colorValue = new HSBType(new DecimalType(value.get("hue").getAsInt()), + new PercentType(value.get("saturation").getAsInt() * 100), + new PercentType(value.get("brightness").getAsInt() * 100)); + } + } + } + if (colorValue != null) { + if (!colorValue.equals(lastColor)) { + result.needSingleUpdate = true; + lastColor = colorValue; + } + } + updateState(COLOR.channelId, colorValue == null ? UnDefType.UNDEF : colorValue); + } + if (INTERFACE_COLOR_PROPERTIES.equals(interfaceName)) { + String colorNameValue = null; + for (JsonObject state : stateList) { + if (COLOR_PROPERTIES.propertyName.equals(state.get("name").getAsString())) { + if (colorNameValue == null) { + result.needSingleUpdate = false; + colorNameValue = state.get("value").getAsJsonObject().get("name").getAsString(); + } + } + } + if (lastColorName == null) { + lastColorName = colorNameValue; + } else if (colorNameValue == null && lastColorName != null) { + colorNameValue = lastColorName; + } + updateState(COLOR_PROPERTIES.channelId, + lastColorName == null ? UnDefType.UNDEF : new StringType(lastColorName)); + } + } + + @Override + public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, + SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException { + if (channelId.equals(COLOR.channelId)) { + if (containsCapabilityProperty(capabilties, COLOR.propertyName)) { + if (command instanceof HSBType) { + HSBType color = ((HSBType) command); + JsonObject colorObject = new JsonObject(); + colorObject.addProperty("hue", color.getHue()); + colorObject.addProperty("saturation", color.getSaturation().floatValue() / 100); + colorObject.addProperty("brightness", color.getBrightness().floatValue() / 100); + connection.smartHomeCommand(entityId, "setColor", "color", colorObject); + } + } + } + if (channelId.equals(COLOR_PROPERTIES.channelId)) { + if (containsCapabilityProperty(capabilties, COLOR.propertyName)) { + if (command instanceof StringType) { + String colorName = ((StringType) command).toFullString(); + if (StringUtils.isNotEmpty(colorName)) { + lastColorName = colorName; + connection.smartHomeCommand(entityId, "setColor", "colorName", colorName); + return true; + } + } + } + } + return false; + } + + @Override + public @Nullable StateDescription findStateDescription(String channelId, StateDescription originalStateDescription, + @Nullable Locale locale) { + return null; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorTemperatureController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorTemperatureController.java new file mode 100644 index 0000000000000..29a267c1f056b --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorTemperatureController.java @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_NUMBER; +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_STRING; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.StateDescription; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; + +import com.google.gson.JsonObject; + +/** + * The {@link HandlerColorTemperatureController} is responsible for the Alexa.ColorTemperatureController + * + * @author Lukas Knoeller - Initial contribution + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class HandlerColorTemperatureController extends HandlerBase { + // Interface + public static final String INTERFACE = "Alexa.ColorTemperatureController"; + public static final String INTERFACE_COLOR_PROPERTIES = "Alexa.ColorPropertiesController"; + + // Channel types + private static final ChannelTypeUID CHANNEL_TYPE_COLOR_TEMPERATURE_NAME = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "colorTemperatureName"); + + private static final ChannelTypeUID CHANNEL_TYPE_COLOR_TEPERATURE_IN_KELVIN = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "colorTemperatureInKelvin"); + + // Channel and Properties + private static final ChannelInfo COLOR_TEMPERATURE_IN_KELVIN = new ChannelInfo( + "colorTemperatureInKelvin" /* propertyName */ , "colorTemperatureInKelvin" /* ChannelId */, + CHANNEL_TYPE_COLOR_TEPERATURE_IN_KELVIN /* Channel Type */ , ITEM_TYPE_NUMBER /* Item Type */); + + private static final ChannelInfo COLOR_TEMPERATURE_NAME = new ChannelInfo("colorProperties" /* propertyName */ , + "colorTemperatureName" /* ChannelId */, CHANNEL_TYPE_COLOR_TEMPERATURE_NAME /* Channel Type */ , + ITEM_TYPE_STRING /* Item Type */); + + private @Nullable Integer lastColorTemperature; + private @Nullable String lastColorName; + + @Override + public String[] getSupportedInterface() { + return new String[] { INTERFACE, INTERFACE_COLOR_PROPERTIES }; + } + + @Override + protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + if (COLOR_TEMPERATURE_IN_KELVIN.propertyName.contentEquals(property)) { + return new ChannelInfo[] { COLOR_TEMPERATURE_IN_KELVIN, COLOR_TEMPERATURE_NAME }; + } + return null; + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + if (INTERFACE.equals(interfaceName)) { + Integer colorTemperatureInKelvinValue = null; + for (JsonObject state : stateList) { + if (COLOR_TEMPERATURE_IN_KELVIN.propertyName.equals(state.get("name").getAsString())) { + int value = state.get("value").getAsInt(); + // For groups take the maximum + if (colorTemperatureInKelvinValue == null) { + colorTemperatureInKelvinValue = value; + } + } + } + if (colorTemperatureInKelvinValue != null && !colorTemperatureInKelvinValue.equals(lastColorTemperature)) { + lastColorTemperature = colorTemperatureInKelvinValue; + result.needSingleUpdate = true; + } + updateState(COLOR_TEMPERATURE_IN_KELVIN.channelId, colorTemperatureInKelvinValue == null ? UnDefType.UNDEF + : new DecimalType(colorTemperatureInKelvinValue)); + } + if (INTERFACE_COLOR_PROPERTIES.equals(interfaceName)) { + String colorTemperatureNameValue = null; + for (JsonObject state : stateList) { + if (COLOR_TEMPERATURE_NAME.propertyName.equals(state.get("name").getAsString())) { + if (colorTemperatureNameValue == null) { + result.needSingleUpdate = false; + colorTemperatureNameValue = state.get("value").getAsJsonObject().get("name").getAsString(); + } + } + } + if (lastColorName == null) { + lastColorName = colorTemperatureNameValue; + } else if (colorTemperatureNameValue == null && lastColorName != null) { + colorTemperatureNameValue = lastColorName; + } + updateState(COLOR_TEMPERATURE_NAME.channelId, + colorTemperatureNameValue == null ? UnDefType.UNDEF : new StringType(colorTemperatureNameValue)); + } + } + + @Override + public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, + SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException { + if (channelId.equals(COLOR_TEMPERATURE_IN_KELVIN.channelId)) { + // WRITING TO THIS CHANNEL DOES CURRENTLY NOT WORK, BUT WE LEAVE THE CODE FOR FUTURE USE! + if (containsCapabilityProperty(capabilties, COLOR_TEMPERATURE_IN_KELVIN.propertyName)) { + if (command instanceof DecimalType) { + int intValue = ((DecimalType) command).intValue(); + if (intValue < 1000) { + intValue = 1000; + } + if (intValue > 10000) { + intValue = 10000; + } + connection.smartHomeCommand(entityId, "setColorTemperature", "colorTemperatureInKelvin", intValue); + return true; + } + } + } + if (channelId.equals(COLOR_TEMPERATURE_NAME.channelId)) { + if (containsCapabilityProperty(capabilties, COLOR_TEMPERATURE_IN_KELVIN.propertyName)) { + if (command instanceof StringType) { + String colorTemperatureName = ((StringType) command).toFullString(); + if (StringUtils.isNotEmpty(colorTemperatureName)) { + lastColorName = colorTemperatureName; + connection.smartHomeCommand(entityId, "setColorTemperature", "colorTemperatureName", + colorTemperatureName); + return true; + } + } + } + } + return false; + } + + @Override + public @Nullable StateDescription findStateDescription(String channelUID, StateDescription originalStateDescription, + @Nullable Locale locale) { + return null; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPercentageController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPercentageController.java new file mode 100644 index 0000000000000..11037a893acbc --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPercentageController.java @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_DIMMER; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.StateDescription; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; + +import com.google.gson.JsonObject; + +/** + * The {@link HandlerPercentageController} is responsible for the Alexa.PowerControllerInterface + * + * @author Lukas Knoeller - Initial contribution + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class HandlerPercentageController extends HandlerBase { + // Interface + public static final String INTERFACE = "Alexa.PercentageController"; + + // Channel types + private static final ChannelTypeUID CHANNEL_TYPE_PERCENTAGE = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "percentage"); + + // Channel definitions + private static final ChannelInfo PERCENTAGE = new ChannelInfo("percentage" /* propertyName */ , + "percentage" /* ChannelId */, CHANNEL_TYPE_PERCENTAGE /* Channel Type */ , + ITEM_TYPE_DIMMER /* Item Type */); + + private @Nullable Integer lastPercentage; + + @Override + public String[] getSupportedInterface() { + return new String[] { INTERFACE }; + } + + @Override + protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + if (PERCENTAGE.propertyName.equals(property)) { + return new ChannelInfo[] { PERCENTAGE }; + } + return null; + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + Integer percentageValue = null; + for (JsonObject state : stateList) { + if (PERCENTAGE.propertyName.equals(state.get("name").getAsString())) { + int value = state.get("value").getAsInt(); + // For groups take the maximum + if (percentageValue == null) { + percentageValue = value; + } else if (value > percentageValue) { + percentageValue = value; + } + } + } + if (percentageValue != null) { + lastPercentage = percentageValue; + } + updateState(PERCENTAGE.channelId, percentageValue == null ? UnDefType.UNDEF : new PercentType(percentageValue)); + } + + @Override + public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, + SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException { + if (channelId.equals(PERCENTAGE.channelId)) { + if (containsCapabilityProperty(capabilties, PERCENTAGE.propertyName)) { + if (command.equals(IncreaseDecreaseType.INCREASE)) { + Integer lastPercentage = this.lastPercentage; + if (lastPercentage != null) { + int newValue = lastPercentage++; + if (newValue > 100) { + newValue = 100; + } + this.lastPercentage = newValue; + connection.smartHomeCommand(entityId, "setPercentage", PERCENTAGE.propertyName, newValue); + return true; + } + } else if (command.equals(IncreaseDecreaseType.DECREASE)) { + Integer lastPercentage = this.lastPercentage; + if (lastPercentage != null) { + int newValue = lastPercentage--; + if (newValue < 0) { + newValue = 0; + } + this.lastPercentage = newValue; + connection.smartHomeCommand(entityId, "setPercentage", PERCENTAGE.propertyName, newValue); + return true; + } + } else if (command.equals(OnOffType.OFF)) { + lastPercentage = 0; + connection.smartHomeCommand(entityId, "setPercentage", PERCENTAGE.propertyName, 0); + return true; + } else if (command.equals(OnOffType.ON)) { + lastPercentage = 100; + connection.smartHomeCommand(entityId, "setPercentage", PERCENTAGE.propertyName, 100); + return true; + } else if (command instanceof PercentType) { + lastPercentage = ((PercentType) command).intValue(); + connection.smartHomeCommand(entityId, "setPercentage", PERCENTAGE.propertyName, lastPercentage); + return true; + } + } + } + return false; + } + + @Override + public @Nullable StateDescription findStateDescription(String channelId, StateDescription originalStateDescription, + @Nullable Locale locale) { + return null; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerController.java new file mode 100644 index 0000000000000..9ff1d0c325417 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerController.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_SWITCH; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.StateDescription; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; + +import com.google.gson.JsonObject; + +/** + * The {@link HandlerPowerController} is responsible for the Alexa.PowerControllerInterface + * + * @author Lukas Knoeller - Initial contribution + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class HandlerPowerController extends HandlerBase { + // Interface + public static final String INTERFACE = "Alexa.PowerController"; + + // Channel types + private static final ChannelTypeUID CHANNEL_TYPE_POWER_STATE = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "powerState"); + + // Channel definitions + private static final ChannelInfo POWER_STATE = new ChannelInfo("powerState" /* propertyName */ , + "powerState" /* ChannelId */, CHANNEL_TYPE_POWER_STATE /* Channel Type */ , + ITEM_TYPE_SWITCH /* Item Type */); + + @Override + public String[] getSupportedInterface() { + return new String[] { INTERFACE }; + } + + @Override + protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + if (POWER_STATE.propertyName.equals(property)) { + return new ChannelInfo[] { POWER_STATE }; + } + return null; + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + Boolean powerStateValue = null; + for (JsonObject state : stateList) { + if (POWER_STATE.propertyName.equals(state.get("name").getAsString())) { + String value = state.get("value").getAsString(); + // For groups take true if all true + if ("ON".equals(value)) { + powerStateValue = true; + } else if (powerStateValue == null) { + powerStateValue = false; + } + + } + } + updateState(POWER_STATE.channelId, + powerStateValue == null ? UnDefType.UNDEF : (powerStateValue ? OnOffType.ON : OnOffType.OFF)); + } + + @Override + public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, + SmartHomeCapability[] capabilities, String channelId, Command command) throws IOException { + if (channelId.equals(POWER_STATE.channelId)) { + if (containsCapabilityProperty(capabilities, POWER_STATE.propertyName)) { + if (command.equals(OnOffType.ON)) { + connection.smartHomeCommand(entityId, "turnOn"); + return true; + } else if (command.equals(OnOffType.OFF)) { + connection.smartHomeCommand(entityId, "turnOff"); + return true; + } + } + } + return false; + } + + @Override + public @Nullable StateDescription findStateDescription(String channelId, StateDescription originalStateDescription, + @Nullable Locale locale) { + return null; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerLevelController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerLevelController.java new file mode 100644 index 0000000000000..d9ae0c33eb86c --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerLevelController.java @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_DIMMER; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.StateDescription; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; + +import com.google.gson.JsonObject; + +/** + * The {@link HandlerPowerLevelController} is responsible for the Alexa.PowerControllerInterface + * + * @author Lukas Knoeller - Initial contribution + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class HandlerPowerLevelController extends HandlerBase { + // Interface + public static final String INTERFACE = "Alexa.PowerLevelController"; + + // Channel types + private static final ChannelTypeUID CHANNEL_TYPE_POWER_LEVEL = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "powerLevel"); + + // Channel definitions + private static final ChannelInfo POWER_LEVEL = new ChannelInfo("powerLevel" /* propertyName */ , + "powerLevel" /* ChannelId */, CHANNEL_TYPE_POWER_LEVEL /* Channel Type */ , + ITEM_TYPE_DIMMER /* Item Type */); + + private @Nullable Integer lastPowerLevel; + + @Override + public String[] getSupportedInterface() { + return new String[] { INTERFACE }; + } + + @Override + protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + if (POWER_LEVEL.propertyName.equals(property)) { + return new ChannelInfo[] { POWER_LEVEL }; + } + return null; + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + Integer powerLevelValue = null; + for (JsonObject state : stateList) { + if (POWER_LEVEL.propertyName.equals(state.get("name").getAsString())) { + int value = state.get("value").getAsInt(); + // For groups take the maximum + if (powerLevelValue == null) { + powerLevelValue = value; + } else if (value > powerLevelValue) { + powerLevelValue = value; + } + } + } + if (powerLevelValue != null) { + lastPowerLevel = powerLevelValue; + } + updateState(POWER_LEVEL.channelId, + powerLevelValue == null ? UnDefType.UNDEF : new PercentType(powerLevelValue)); + } + + @Override + public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, + SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException { + if (channelId.equals(POWER_LEVEL.channelId)) { + if (containsCapabilityProperty(capabilties, POWER_LEVEL.propertyName)) { + if (command.equals(IncreaseDecreaseType.INCREASE)) { + Integer lastPowerLevel = this.lastPowerLevel; + if (lastPowerLevel != null) { + int newValue = lastPowerLevel++; + if (newValue > 100) { + newValue = 100; + } + this.lastPowerLevel = newValue; + connection.smartHomeCommand(entityId, "setPowerLevel", POWER_LEVEL.propertyName, newValue); + return true; + } + } else if (command.equals(IncreaseDecreaseType.DECREASE)) { + Integer lastPowerLevel = this.lastPowerLevel; + if (lastPowerLevel != null) { + int newValue = lastPowerLevel--; + if (newValue < 0) { + newValue = 0; + } + this.lastPowerLevel = newValue; + connection.smartHomeCommand(entityId, "setPowerLevel", POWER_LEVEL.propertyName, newValue); + return true; + } + } else if (command.equals(OnOffType.OFF)) { + lastPowerLevel = 0; + connection.smartHomeCommand(entityId, "setPowerLevel", POWER_LEVEL.propertyName, 0); + return true; + } else if (command.equals(OnOffType.ON)) { + lastPowerLevel = 100; + connection.smartHomeCommand(entityId, "setPowerLevel", POWER_LEVEL.propertyName, 100); + return true; + } else if (command instanceof PercentType) { + lastPowerLevel = ((PercentType) command).intValue(); + connection.smartHomeCommand(entityId, "setPowerLevel", POWER_LEVEL.propertyName, + ((PercentType) command).floatValue() / 100); + return true; + } + } + } + return false; + } + + @Override + public @Nullable StateDescription findStateDescription(String channelId, StateDescription originalStateDescription, + @Nullable Locale locale) { + return null; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java new file mode 100644 index 0000000000000..adeb0da2a3666 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_CONTACT; +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_STRING; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.OpenClosedType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.StateDescription; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; + +import com.google.gson.JsonObject; + +/** + * The {@link HandlerSecurityPanelController} is responsible for the Alexa.PowerControllerInterface + * + * @author Lukas Knoeller - Initial contribution + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class HandlerSecurityPanelController extends HandlerBase { + // Interface + public static final String INTERFACE = "Alexa.SecurityPanelController"; + + // Channel types + private static final ChannelTypeUID CHANNEL_TYPE_ARM_STATE = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "armState"); + + private static final ChannelTypeUID CHANNEL_TYPE_BURGLARY_ALARM = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "burglaryAlarm"); + + private static final ChannelTypeUID CHANNEL_TYPE_CARBON_MONOXIDE_ALARM = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "carbonMonoxideAlarm"); + + private static final ChannelTypeUID CHANNEL_TYPE_FIRE_ALARM = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "fireAlarm"); + + private static final ChannelTypeUID CHANNEL_TYPE_WATER_ALARM = new ChannelTypeUID( + AmazonEchoControlBindingConstants.BINDING_ID, "waterAlarm"); + + // Channel definitions + private static final ChannelInfo ARM_STATE = new ChannelInfo("armState" /* propertyName */ , + "armState" /* ChannelId */, CHANNEL_TYPE_ARM_STATE /* Channel Type */ , ITEM_TYPE_STRING /* Item Type */); + + private static final ChannelInfo BURGLARY_ALARM = new ChannelInfo("burglaryAlarm" /* propertyName */ , + "burglaryAlarm" /* ChannelId */, CHANNEL_TYPE_BURGLARY_ALARM /* Channel Type */ , + ITEM_TYPE_CONTACT /* Item Type */); + + private static final ChannelInfo CARBON_MONOXIDE_ALARM = new ChannelInfo("carbonMonoxideAlarm" /* propertyName */ , + "carbonMonoxideAlarm" /* ChannelId */, CHANNEL_TYPE_CARBON_MONOXIDE_ALARM /* Channel Type */ , + ITEM_TYPE_CONTACT /* Item Type */); + + private static final ChannelInfo FIRE_ALARM = new ChannelInfo("fireAlarm" /* propertyName */ , + "fireAlarm" /* ChannelId */, CHANNEL_TYPE_FIRE_ALARM /* Channel Type */ , + ITEM_TYPE_CONTACT /* Item Type */); + + private static final ChannelInfo WATER_ALARM = new ChannelInfo("waterAlarm" /* propertyName */ , + "waterAlarm" /* ChannelId */, CHANNEL_TYPE_WATER_ALARM /* Channel Type */ , + ITEM_TYPE_CONTACT /* Item Type */); + + private ChannelInfo[] getAlarmChannels() { + return new ChannelInfo[] { BURGLARY_ALARM, CARBON_MONOXIDE_ALARM, FIRE_ALARM, WATER_ALARM }; + } + + @Override + public String[] getSupportedInterface() { + return new String[] { INTERFACE }; + } + + @Override + protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + if (ARM_STATE.propertyName.equals(property)) { + return new ChannelInfo[] { ARM_STATE }; + } + for (ChannelInfo channelInfo : getAlarmChannels()) { + if (channelInfo.propertyName.equals(property)) { + return new ChannelInfo[] { channelInfo }; + } + } + return null; + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + String armStateValue = null; + Boolean burglaryAlarmValue = null; + Boolean carbonMonoxideAlarmValue = null; + Boolean fireAlarmValue = null; + Boolean waterAlarmValue = null; + for (JsonObject state : stateList) { + if (ARM_STATE.propertyName.equals(state.get("name").getAsString())) { + if (armStateValue == null) { + armStateValue = state.get("value").getAsString(); + } + } else if (BURGLARY_ALARM.propertyName.equals(state.get("name").getAsString())) { + if (burglaryAlarmValue == null) { + burglaryAlarmValue = "ALARM".equals(state.get("value").getAsString()); + } + } else if (CARBON_MONOXIDE_ALARM.propertyName.equals(state.get("name").getAsString())) { + if (carbonMonoxideAlarmValue == null) { + carbonMonoxideAlarmValue = "ALARM".equals(state.get("value").getAsString()); + } + } else if (FIRE_ALARM.propertyName.equals(state.get("name").getAsString())) { + if (fireAlarmValue == null) { + fireAlarmValue = "ALARM".equals(state.get("value").getAsString()); + } + } else if (WATER_ALARM.propertyName.equals(state.get("name").getAsString())) { + if (waterAlarmValue == null) { + waterAlarmValue = "ALARM".equals(state.get("value").getAsString()); + } + } + } + updateState(ARM_STATE.channelId, armStateValue == null ? UnDefType.UNDEF : new StringType(armStateValue)); + updateState(BURGLARY_ALARM.channelId, burglaryAlarmValue == null ? UnDefType.UNDEF + : (burglaryAlarmValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); + updateState(CARBON_MONOXIDE_ALARM.channelId, carbonMonoxideAlarmValue == null ? UnDefType.UNDEF + : (carbonMonoxideAlarmValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); + updateState(FIRE_ALARM.channelId, fireAlarmValue == null ? UnDefType.UNDEF + : (fireAlarmValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); + updateState(WATER_ALARM.channelId, waterAlarmValue == null ? UnDefType.UNDEF + : (waterAlarmValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); + } + + @Override + public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, + SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException { + if (channelId.equals(ARM_STATE.channelId)) { + if (containsCapabilityProperty(capabilties, ARM_STATE.propertyName)) { + if (command instanceof StringType) { + String armStateValue = ((StringType) command).toFullString(); + if (StringUtils.isNotEmpty(armStateValue)) { + connection.smartHomeCommand(entityId, "controlSecurityPanel", ARM_STATE.propertyName, + armStateValue); + return true; + } + } + } + } + return false; + } + + @Override + public @Nullable StateDescription findStateDescription(String channelUID, StateDescription originalStateDescription, + @Nullable Locale locale) { + return null; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerTemperatureSensor.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerTemperatureSensor.java new file mode 100644 index 0000000000000..5610ef858e259 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerTemperatureSensor.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.CHANNEL_TYPE_TEMPERATURE; +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_NUMBER_TEMPERATURE; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.unit.ImperialUnits; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.StateDescription; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; + +import com.google.gson.JsonObject; + +/** + * The {@link HandlerTemperatureSensor} is responsible for the Alexa.PowerControllerInterface + * + * @author Lukas Knoeller - Initial contribution + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class HandlerTemperatureSensor extends HandlerBase { + // Interface + public static final String INTERFACE = "Alexa.TemperatureSensor"; + // Channel definitions + private static final ChannelInfo TEMPERATURE = new ChannelInfo("temperature" /* propertyName */ , + "temperature" /* ChannelId */, CHANNEL_TYPE_TEMPERATURE /* Channel Type */ , + ITEM_TYPE_NUMBER_TEMPERATURE /* Item Type */); + + @Override + public String[] getSupportedInterface() { + return new String[] { INTERFACE }; + } + + @Override + protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + if (TEMPERATURE.propertyName.equals(property)) { + return new ChannelInfo[] { TEMPERATURE }; + } + return null; + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + QuantityType temperatureValue = null; + for (JsonObject state : stateList) { + if (TEMPERATURE.propertyName.equals(state.get("name").getAsString())) { + JsonObject value = state.get("value").getAsJsonObject(); + // For groups take the first + if (temperatureValue == null) { + int temperature = value.get("value").getAsInt(); + String scale = value.get("scale").getAsString(); + if ("CELSIUS".equals(scale)) { + temperatureValue = new QuantityType(temperature, SIUnits.CELSIUS); + } else { + temperatureValue = new QuantityType(temperature, ImperialUnits.FAHRENHEIT); + } + } + } + } + updateState(TEMPERATURE.channelId, temperatureValue == null ? UnDefType.UNDEF : temperatureValue); + } + + @Override + public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, + SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException { + return false; + } + + @Override + public @Nullable StateDescription findStateDescription(String channelId, StateDescription originalStateDescription, + @Nullable Locale locale) { + return null; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/SmartHomeDeviceStateGroupUpdateCalculator.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/SmartHomeDeviceStateGroupUpdateCalculator.java new file mode 100644 index 0000000000000..40dab399d7ae6 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/SmartHomeDeviceStateGroupUpdateCalculator.java @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.DriverIdentity; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles the update interval calculation + * + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class SmartHomeDeviceStateGroupUpdateCalculator { + private final Logger logger = LoggerFactory.getLogger(SmartHomeDeviceStateGroupUpdateCalculator.class); + + private static final Integer UPDATE_INTERVAL_PRIVATE_SKILLS_IN_SECONDS = 10; + private static final Integer UPDATE_INTERVAL_PRIVATE_SKILLS_IN_SECONDS_TRACE = 600; + private static final Integer UPDATE_INTERVAL_ACOUSTIC_EVENTS_IN_SECONDS = 10; + private Integer updateIntervalAmazonInSeconds; + private Integer updateIntervalSkillsInSeconds; + + private static class UpdateGroup { + private final int intervalInSeconds; + private Date lastUpdated; + + public UpdateGroup(int intervalInSeconds) { + this.intervalInSeconds = intervalInSeconds; + this.lastUpdated = new Date(0); + } + } + + private final Map updateGroups = new HashMap<>(); + + public SmartHomeDeviceStateGroupUpdateCalculator(int updateIntervalAmazonInSeconds, + int updateIntervalSkillsInSeconds) { + this.updateIntervalAmazonInSeconds = updateIntervalAmazonInSeconds; + this.updateIntervalSkillsInSeconds = updateIntervalSkillsInSeconds; + } + + private Integer getUpdateIntervalInSeconds(SmartHomeDevice shd) { + Integer updateIntervalInSeconds = shd.updateIntervalInSeconds; + if (updateIntervalInSeconds != null) { + return updateIntervalInSeconds; + } + SmartHomeCapability[] capabilities = shd.capabilities; + if (capabilities != null) { + for (SmartHomeCapability capability : capabilities) { + if (capability != null && HandlerAcousticEventSensor.INTERFACE.equals(capability.interfaceName)) { + updateIntervalInSeconds = UPDATE_INTERVAL_ACOUSTIC_EVENTS_IN_SECONDS; + break; + } + } + } + if (updateIntervalInSeconds == null) { + if ("openHAB".equalsIgnoreCase(shd.manufacturerName) + || StringUtils.startsWithIgnoreCase(shd.manufacturerName, "ioBroker")) { + // OpenHAB or ioBroker skill + if (logger.isTraceEnabled()) { + updateIntervalInSeconds = UPDATE_INTERVAL_PRIVATE_SKILLS_IN_SECONDS_TRACE; + } else { + updateIntervalInSeconds = UPDATE_INTERVAL_PRIVATE_SKILLS_IN_SECONDS; + } + } else { + boolean isSkillDevice = false; + DriverIdentity driverIdentity = shd.driverIdentity; + isSkillDevice = driverIdentity != null && "SKILL".equals(driverIdentity.namespace); + if (isSkillDevice) { + updateIntervalInSeconds = updateIntervalSkillsInSeconds; + } else { + updateIntervalInSeconds = updateIntervalAmazonInSeconds; + } + } + } + shd.updateIntervalInSeconds = updateIntervalInSeconds; + return updateIntervalInSeconds; + } + + public void removeDevicesWithNoUpdate(List devices) { + Date updateTimeStamp = new Date(); + // check if new group is needed + boolean syncAllGroups = false; + for (SmartHomeDevice device : devices) { + int updateIntervalInSeconds = getUpdateIntervalInSeconds(device); + if (!updateGroups.containsKey(updateIntervalInSeconds)) { + UpdateGroup newGroup = new UpdateGroup(updateIntervalInSeconds); + updateGroups.put(updateIntervalInSeconds, newGroup); + syncAllGroups = true; + } + } + // check which groups needs an update + Set groupsToUpdate = new HashSet(); + for (UpdateGroup group : updateGroups.values()) { + long millisecondsSinceLastUpdate = updateTimeStamp.getTime() - group.lastUpdated.getTime(); + if (syncAllGroups || millisecondsSinceLastUpdate >= group.intervalInSeconds * 1000) { + group.lastUpdated = updateTimeStamp; + groupsToUpdate.add(group.intervalInSeconds); + } + } + // remove unused devices + for (int i = devices.size() - 1; i >= 0; i--) { + if (!groupsToUpdate.contains(getUpdateIntervalInSeconds(devices.get(i)))) { + devices.remove(i); + } + } + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/ESH-INF/binding/binding.xml index 6036baea74da3..a51d4eadc0930 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/ESH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/ESH-INF/binding/binding.xml @@ -6,6 +6,6 @@ Amazon Echo Control Binding Binding to control Amazon Echo devices (Alexa). This binding enables openHAB to control the volume, playing state, bluetooth connection of your amazon echo devices or allow to use it as TTS device. - Michael Geramb + Michael Geramb, Lukas Knoeller diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/ESH-INF/i18n/amazonechocontrol_de.properties b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/ESH-INF/i18n/amazonechocontrol_de.properties index 699cd51b94144..1661f411aa410 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/ESH-INF/i18n/amazonechocontrol_de.properties +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/ESH-INF/i18n/amazonechocontrol_de.properties @@ -6,6 +6,13 @@ binding.amazonechocontrol.description = Binding zum Steuern von Amazon Echo (Ale thing-type.amazonechocontrol.account.label = Amazon Konto thing-type.amazonechocontrol.account.description = Amazon Konto bei dem die Amazon Echo Gerte registriert sind. +thing-type.amazonechocontrol.account.config-description.discoverSmartHome.description = Definiert ob openHAB Gerte die mit Amazon Echo verbunden sind, gefunden werden sollen. +thing-type.amazonechocontrol.account.config-description.discoverSmartHome.option.0 = Nicht finden +thing-type.amazonechocontrol.account.config-description.discoverSmartHome.option.1 = Direkt verbundene +thing-type.amazonechocontrol.account.config-description.discoverSmartHome.option.2 = Direkt und ber Skill verbundene +thing-type.amazonechocontrol.account.config-description.pollingIntervalSmartHomeAlexa.description = Definiert das Intervall in Sekunden, wie hufig OpenHAB den Status der mit Alexa verbundenen Gerte abfragt. +thing-type.amazonechocontrol.account.config-description.pollingIntervalSmartSkills.description = Definiert das Intervall in Sekunden, wie hufig OpenHAB den Status der ber Skill Gerte abfragt. +thing-type.amazonechocontrol.account.config-description.discoverOpenHabSmartHomeDevices.description = Definiert ob auch Gerte die ber den openHAB Skill verbunden sind, gefunden werden sollen. Diese Option ist nur fr Entwicklungs- und Testzwecke. thing-type.amazonechocontrol.echo.label = Amazon Echo thing-type.amazonechocontrol.echo.description = Amazon Echo Gert (Amazon Echo, Amazon Echo Dot, Amazon Echo Plus...) @@ -34,6 +41,19 @@ thing-type.config.wha.echoshow.serialNumber.description = Die Seriennummer des G thing-type.amazonechocontrol.flashbriefingprofile.label = Tgliche Zusammenfassungsprofile thing-type.amazonechocontrol.flashbriefingprofile.description = Speichert und ld eine Tgliches Zusammenfassungskonfiguration +thing-type.amazonechocontrol.smartHomeDevice.label = Smart Home Gert +thing-type.amazonechocontrol.smartHomeDevice.description = Ein mit Alexa verbundenes Smart Home Gert + +thing-type.amazonechocontrol.smartHomeDevice.id.label = Gerte Id +thing-type.amazonechocontrol.smartHomeDevice.id.description = Die Id des Gertes (Bitte die Suchfunktion benutzen, um ein konfiguriertes Thing zu bekommen) + +thing-type.amazonechocontrol.smartHomeDeviceGroup.label = Smart Home Gerte Gruppe +thing-type.amazonechocontrol.smartHomeDeviceGroup.description = Eine Gruppe von Smart Home Gerten + +thing-type.amazonechocontrol.smartHomeDevice.id.label = Gruppen Id +thing-type.amazonechocontrol.smartHomeDevice.id.description = Die Id der Gertegruppe (Bitte die Suchfunktion benutzen, um ein konfiguriertes Thing zu bekommen) + + # channel types channel-type.amazonechocontrol.bluetoothDeviceName.label = Bluetooth Gert channel-type.amazonechocontrol.bluetoothDeviceName.description = Verbundenes Bluetoothgert @@ -169,3 +189,44 @@ channel-type.amazonechocontrol.playCommand.option.GoodMorning = Guten Morgen channel-type.amazonechocontrol.playCommand.option.SingASong = Lied channel-type.amazonechocontrol.playCommand.option.TellStory = Geschichte channel-type.amazonechocontrol.playCommand.option.TellStory = Zusammenfassung + +channel-type-amazonechocontrol.colorName.label = Lichtfarbe Name +channel-type-amazonechocontrol.colorName.description = Farbe der smarten Lampe +channel-type-amazonechocontrol.colorName.option.red = Rot +channel-type-amazonechocontrol.colorName.option.crimson = Purpur +channel-type-amazonechocontrol.colorName.option.salmon = Lachs +channel-type-amazonechocontrol.colorName.option.orange = Orange +channel-type-amazonechocontrol.colorName.option.gold = Gold +channel-type-amazonechocontrol.colorName.option.yellow = Gelb +channel-type-amazonechocontrol.colorName.option.green = Grn +channel-type-amazonechocontrol.colorName.option.turquoise = Trkis +channel-type-amazonechocontrol.colorName.option.cyan = Cyan +channel-type-amazonechocontrol.colorName.option.sky_blue = Himmelblau +channel-type-amazonechocontrol.colorName.option.blue = Blau +channel-type-amazonechocontrol.colorName.option.purple = Lila +channel-type-amazonechocontrol.colorName.option.magenta = Magenta +channel-type-amazonechocontrol.colorName.option.pink = Pink +channel-type-amazonechocontrol.colorName.option.lavender = Lavendel + +channel-type-amazonechocontrol.colorTemperatureName.label = Weiton Name +channel-type-amazonechocontrol.colorTemperatureName.description = Weitemperatur der Lampe am Amazon Echo +channel-type-amazonechocontrol.colorTemperatureName.option.warm_white = Warmwei +channel-type-amazonechocontrol.colorTemperatureName.option.soft_white = Sanftes Wei +channel-type-amazonechocontrol.colorTemperatureName.option.white = Wei +channel-type-amazonechocontrol.colorTemperatureName.option.daylight_white = Tageslicht +channel-type-amazonechocontrol.colorTemperatureName.option.cool_white = Kaltwei + +channel-type-amazonechocontrol.brightness.label = Helligkeit +channel-type-amazonechocontrol.brightness.description = Helligkeit der Lampe + +channel-type-amazonechocontrol.powerState.label = Zustand +channel-type-amazonechocontrol.powerState.description = Zustand (Ein/Aus) + +channel-type-amazonechocontrol.armState.label = Zustand +channel-type-amazonechocontrol.armState.description = Zustand (Active/Stay/Night/Deactivated) + +channel-type-amazonechocontrol.armState.option.ARMED_AWAY = Aktiv +channel-type-amazonechocontrol.armState.option.ARMED_STAY = Anwesend +channel-type-amazonechocontrol.armState.option.ARMED_NIGHT = Nacht +channel-type-amazonechocontrol.armState.option.DISARMED = Deativiert + diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/ESH-INF/thing/thing-types.xml index 0618a2b93f8ce..4758bb3d74a6b 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/ESH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/ESH-INF/thing/thing-types.xml @@ -1,471 +1,530 @@ - - - - - Amazon Account where the amazon echo devices are registered. - - - - - - - - - - Amazon Echo device (Amazon Echo, Amazon Echo Dot, Amazon Echo Plus...) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - The serial number of the device from the Alexa app - - - - - - - - - Amazon Echo Spot device - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - The serial number of the device from the Alexa app - - - - - - - - - Amazon Echo Show device - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - The serial number of the device from the Alexa app - - - - - - - - - Amazon Multiroom Music - - - - - - - - - - - - - - - - - - serialNumber - - - - The serial number of the device from the Alexa app - - - - - - - - - - Store and load a flash briefing configuration - - - - - - - - Switch - - Save the current flash briefing configuration (Write only) - - - Switch - - Activate this flash briefing configuration - - - String - - Plays the briefing on the device (serial number or name, write only) - - - String - - Connected bluetooth device - - - - String - - Id of the radio station - - - String - - Speak the reminder and send a notification to the Alexa app - - - String - - The command which must be spoken to active the routing without the preceding "Alexa," (Write Only) - - - - String - - Plays an alarm sound - - - String - - Id of the amazon music track - - - Switch - - Amazon Music turned on - - - String - - Amazon Music play list id (Write only, no current state) - - - String - - Id of the playlist which was started with openHAB - - - String - - Name of music provider - - - - String - - MAC-Address of the bluetooth connected device - - - String - - Url of the album image or radio station logo - - - - String - - Title - - - - String - - Subtitle 1 - - - - String - - Subtitle 2 - - - - Switch - - Radio turned on - - - Switch - - Connect to last used device - - - Switch - - Loop - - - Switch - - Shuffle play - - - Player - - Music Player - - - Dimmer - - Volume of the sound - - - Number - - Equalizer Treble - - - - Number - - Equalizer Midrange - - - - Number - - Equalizer Bass - - - - String - - Music provider - - - String - - Voice command as text. E.g. 'Yesterday from the Beatles' (Write only) - - - String - - Sends a message to the Echo devices (Write only). - - - String - - Display the announcement message on the display (Write only). See in the tutorial section of the binding - description to learn how it's possible to set the title and turn off the sound. - - - String - - Speak the text (Write only). It is possible to use plain text or SSML: <speak>I want to tell you a - secret.<amazon:effect name="whispered">I am not a real human.</amazon:effect>.Can you believe - it?</speak> - - - Dimmer - - Volume of the Speak channel. If 0, the current volume will be used. - - - String - - Last voice command spoken to the device. Writing to the channel starts voice output. - - - - Dimmer - - Media progress in percent - - - - Number:Time - - Media play time - - - - Number:Time - - Media length - - - - String - - Start information (Write only) - - - - - - - - - - - - - Dimmer - - Notification Volume - - - - Switch - - Ascending alarm up to the configured volume - - - - DateTime - - Next Reminder - - - - DateTime - - Next alarm - - - - DateTime - - Next music alarm - - - - DateTime - - Next timer - - - + + + + + Amazon Account where the amazon echo devices are registered. + + + + + + + Defines which devices shall be discovered. + + + + + + + true + 0 + + + + + Defines the time in seconds for openHAB to pull the + state of the directly connected devices. The minimum + is 10 seconds. + + 30 + + + + + Defines the time in seconds for openHAB to pull the + state of the over a skill connected devices. The + minimum is 60 seconds. + + 120 + + + + + + + + + Amazon Echo device (Amazon Echo, Amazon Echo Dot, Amazon Echo Plus...) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + The serial number of the device from the Alexa app + + + + + + + + + Amazon Echo Spot device + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + The serial number of the device from the Alexa app + + + + + + + + + Amazon Echo Show device + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + The serial number of the device from the Alexa app + + + + + + + + + Amazon Multiroom Music + + + + + + + + + + + + + + + + + + serialNumber + + + + The serial number of the device from the Alexa app + + + + + + + + + Store and load a flash briefing configuration + + + + + + + + + + + + Smart home device connected to Alexa + id + + + + The id of the device (Please use the discover function to get a configured Thing) + + + + + + + + + Group of smart home devices in your amazon account + id + + + + The id of the device group (Please use the discover function to get a configured Thing) + + + + + Switch + + Save the current flash briefing configuration (Write only) + + + Switch + + Activate this flash briefing configuration + + + String + + Plays the briefing on the device (serial number or name, write only) + + + String + + Connected bluetooth device + + + + String + + Id of the radio station + + + String + + Speak the reminder and send a notification to the Alexa app + + + String + + The command which must be spoken to active the routing without the preceding "Alexa," (Write Only) + + + + String + + Plays an alarm sound + + + String + + Id of the amazon music track + + + Switch + + Amazon Music turned on + + + String + + Amazon Music play list id (Write only, no current state) + + + String + + Id of the playlist which was started with openHAB + + + String + + Name of music provider + + + + String + + MAC-Address of the bluetooth connected device + + + String + + Url of the album image or radio station logo + + + + String + + Title + + + + String + + Subtitle 1 + + + + String + + Subtitle 2 + + + + Switch + + Radio turned on + + + Switch + + Connect to last used device + + + Switch + + Loop + + + Switch + + Shuffle play + + + Player + + Music Player + + + Dimmer + + Volume of the sound + + + Number + + Equalizer Treble + + + + Number + + Equalizer Midrange + + + + Number + + Equalizer Bass + + + + String + + Music provider + + + String + + Voice command as text. E.g. 'Yesterday from the Beatles' (Write only) + + + String + + Sends a message to the Echo devices (Write only). + + + String + + Display the announcement message on the display (Write only). See in the tutorial section of the binding + description to learn how it's possible to set the title and turn off the sound. + + + String + + Speak the text (Write only). It is possible to use plain text or SSML: <speak>I want to tell you a + secret.<amazon:effect name="whispered">I am not a real human.</amazon:effect>.Can you believe + it?</speak> + + + Dimmer + + Volume of the Speak channel. If 0, the current volume will be used. + + + String + + Last voice command spoken to the device. Writing to the channel starts voice output. + + + + Dimmer + + Media progress in percent + + + + Number:Time + + Media play time + + + + Number:Time + + Media length + + + + String + + Start information (Write only) + + + + + + + + + + + + + Dimmer + + Notification Volume + + + + Switch + + Ascending alarm up to the configured volume + + + + DateTime + + Next Reminder + + + + DateTime + + Next alarm + + + + DateTime + + Next music alarm + + + + DateTime + + Next timer + + + From 3523c75d7f677b8d1a0e4c0f355843c9c9b435f5 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Wed, 8 Jul 2020 06:14:10 +0200 Subject: [PATCH 40/85] [deconz] Add dynamic state descriptions and fix property updates (#8055) * add dynamic state descriptions and fix property updates * fixes and improvements Signed-off-by: Jan N. Klug --- bundles/org.openhab.binding.deconz/pom.xml | 4 +- .../deconz/internal/DeconzHandlerFactory.java | 7 +- .../internal/StateDescriptionProvider.java | 77 +++++++++++++++++++ .../openhab/binding/deconz/internal/Util.java | 11 --- .../discovery/ThingDiscoveryService.java | 10 +-- .../handler/DeconzBaseThingHandler.java | 2 +- .../internal/handler/LightThingHandler.java | 71 ++++++++++++++--- .../openhab/binding/deconz/LightsTest.java | 41 +++++++++- 8 files changed, 191 insertions(+), 32 deletions(-) create mode 100644 bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/StateDescriptionProvider.java diff --git a/bundles/org.openhab.binding.deconz/pom.xml b/bundles/org.openhab.binding.deconz/pom.xml index 5c13925635ab7..b6bc8130c15c3 100644 --- a/bundles/org.openhab.binding.deconz/pom.xml +++ b/bundles/org.openhab.binding.deconz/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java index 7aa25d9e53a5b..232d11a29d347 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java @@ -59,12 +59,15 @@ public class DeconzHandlerFactory extends BaseThingHandlerFactory { private final Gson gson; private final WebSocketFactory webSocketFactory; private final HttpClientFactory httpClientFactory; + private final StateDescriptionProvider stateDescriptionProvider; @Activate public DeconzHandlerFactory(final @Reference WebSocketFactory webSocketFactory, - final @Reference HttpClientFactory httpClientFactory) { + final @Reference HttpClientFactory httpClientFactory, + final @Reference StateDescriptionProvider stateDescriptionProvider) { this.webSocketFactory = webSocketFactory; this.httpClientFactory = httpClientFactory; + this.stateDescriptionProvider = stateDescriptionProvider; GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer()); @@ -85,7 +88,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { return new DeconzBridgeHandler((Bridge) thing, webSocketFactory, new AsyncHttpClient(httpClientFactory.getCommonHttpClient()), gson); } else if (LightThingHandler.SUPPORTED_THING_TYPE_UIDS.contains(thingTypeUID)) { - return new LightThingHandler(thing, gson); + return new LightThingHandler(thing, gson, stateDescriptionProvider); } else if (SensorThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { return new SensorThingHandler(thing, gson); } else if (SensorThermostatThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/StateDescriptionProvider.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/StateDescriptionProvider.java new file mode 100644 index 0000000000000..7d6378e8d3d09 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/StateDescriptionProvider.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deconz.internal; + +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.type.DynamicStateDescriptionProvider; +import org.eclipse.smarthome.core.types.StateDescription; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Dynamic channel state description provider. + * Overrides the state description for the controls, which receive its configuration in the runtime. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@Component(service = { DynamicStateDescriptionProvider.class, StateDescriptionProvider.class }, immediate = true) +public class StateDescriptionProvider implements DynamicStateDescriptionProvider { + + private final Map descriptions = new ConcurrentHashMap<>(); + private final Logger logger = LoggerFactory.getLogger(StateDescriptionProvider.class); + + /** + * Set a state description for a channel. This description will be used when preparing the channel state by + * the framework for presentation. A previous description, if existed, will be replaced. + * + * @param channelUID + * channel UID + * @param description + * state description for the channel + */ + public void setDescription(ChannelUID channelUID, StateDescription description) { + logger.trace("adding state description for channel {}", channelUID); + descriptions.put(channelUID, description); + } + + /** + * remove all descriptions for a given thing + * + * @param thingUID the thing's UID + */ + public void removeDescriptionsForThing(ThingUID thingUID) { + logger.trace("removing state description for thing {}", thingUID); + descriptions.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID)); + } + + @Override + public @Nullable StateDescription getStateDescription(Channel channel, + @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { + if (descriptions.containsKey(channel.getUID())) { + logger.trace("returning new stateDescription for {}", channel.getUID()); + return descriptions.get(channel.getUID()); + } else { + return null; + } + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/Util.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/Util.java index b49b513995835..ecf5508e0f87a 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/Util.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/Util.java @@ -47,15 +47,4 @@ public static int kelvinToMired(int kelvinValue) { public static int constrainToRange(int intValue, int min, int max) { return Math.max(min, Math.min(intValue, max)); } - - public static int parseIntWithFallback(String text, int defaultValue) { - if (text == null || text.isEmpty()) { - return defaultValue; - } - try { - return Integer.parseInt(text); - } catch (NumberFormatException e) { - return defaultValue; - } - } } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java index 4fbcfc18fdeca..f00151b4e4e12 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java @@ -33,6 +33,7 @@ import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.ThingHandler; import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.openhab.binding.deconz.internal.Util; import org.openhab.binding.deconz.internal.dto.BridgeFullState; import org.openhab.binding.deconz.internal.dto.LightMessage; import org.openhab.binding.deconz.internal.dto.SensorMessage; @@ -120,11 +121,10 @@ private void addLight(String lightID, LightMessage light) { properties.put(Thing.PROPERTY_MODEL_ID, light.modelid); if (light.ctmax != null && light.ctmin != null) { - int ctmax = (light.ctmax > ZCL_CT_MAX) ? ZCL_CT_MAX : light.ctmax; - properties.put(PROPERTY_CT_MAX, Integer.toString(ctmax)); - - int ctmin = (light.ctmin < ZCL_CT_MIN) ? ZCL_CT_MIN : light.ctmin; - properties.put(PROPERTY_CT_MIN, Integer.toString(ctmin)); + properties.put(PROPERTY_CT_MAX, + Integer.toString(Util.constrainToRange(light.ctmax, ZCL_CT_MIN, ZCL_CT_MAX))); + properties.put(PROPERTY_CT_MIN, + Integer.toString(Util.constrainToRange(light.ctmin, ZCL_CT_MIN, ZCL_CT_MAX))); } switch (lightType) { diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java index 9b25a7072ec73..de70bc5d44b07 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java @@ -113,7 +113,7 @@ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { this.http = bridgeHandler.getHttp(); this.bridgeConfig = bridgeHandler.getBridgeConfig(); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING); + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE); // Real-time data registerListener(); diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java index 14dc8994d3d3a..8eee8074d2879 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java @@ -15,6 +15,9 @@ import static org.openhab.binding.deconz.internal.BindingConstants.*; import static org.openhab.binding.deconz.internal.Util.*; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -33,6 +36,10 @@ import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.StateDescription; +import org.eclipse.smarthome.core.types.StateDescriptionFragmentBuilder; +import org.openhab.binding.deconz.internal.StateDescriptionProvider; +import org.openhab.binding.deconz.internal.Util; import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage; import org.openhab.binding.deconz.internal.dto.LightMessage; import org.openhab.binding.deconz.internal.dto.LightState; @@ -70,7 +77,10 @@ public class LightThingHandler extends DeconzBaseThingHandler { private final Logger logger = LoggerFactory.getLogger(LightThingHandler.class); + private final StateDescriptionProvider stateDescriptionProvider; + private long lastCommandExpireTimestamp = 0; + private boolean needsPropertyUpdate = false; /** * The light state. Contains all possible fields for all supported lights @@ -78,13 +88,40 @@ public class LightThingHandler extends DeconzBaseThingHandler { private LightState lightStateCache = new LightState(); private LightState lastCommand = new LightState(); - private final int ct_max; - private final int ct_min; + // set defaults, we can override them later if we receive better values + private int ctMax = ZCL_CT_MAX; + private int ctMin = ZCL_CT_MIN; - public LightThingHandler(Thing thing, Gson gson) { + public LightThingHandler(Thing thing, Gson gson, StateDescriptionProvider stateDescriptionProvider) { super(thing, gson); - ct_max = parseIntWithFallback(thing.getProperties().get(PROPERTY_CT_MAX), ZCL_CT_MAX); - ct_min = parseIntWithFallback(thing.getProperties().get(PROPERTY_CT_MIN), ZCL_CT_MIN); + + this.stateDescriptionProvider = stateDescriptionProvider; + } + + @Override + public void initialize() { + if (thing.getThingTypeUID().equals(THING_TYPE_COLOR_TEMPERATURE_LIGHT) + || thing.getThingTypeUID().equals(THING_TYPE_EXTENDED_COLOR_LIGHT)) { + try { + Map properties = thing.getProperties(); + ctMax = Integer.parseInt(properties.get(PROPERTY_CT_MAX)); + ctMin = Integer.parseInt(properties.get(PROPERTY_CT_MIN)); + + // minimum and maximum are inverted due to mired/kelvin conversion! + StateDescription stateDescription = StateDescriptionFragmentBuilder.create() + .withMinimum(new BigDecimal(miredToKelvin(ctMax))) + .withMaximum(new BigDecimal(miredToKelvin(ctMin))).build().toStateDescription(); + if (stateDescription != null) { + stateDescriptionProvider.setDescription(new ChannelUID(thing.getUID(), CHANNEL_COLOR_TEMPERATURE), + stateDescription); + } else { + logger.warn("Failed to create state description in thing {}", thing.getUID()); + } + } catch (NumberFormatException e) { + needsPropertyUpdate = true; + } + } + super.initialize(); } @Override @@ -174,8 +211,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { break; case CHANNEL_COLOR_TEMPERATURE: if (command instanceof DecimalType) { - int miredValue = kelvinToMired(((DecimalType) command).intValue()); - newLightState.ct = constrainToRange(miredValue,ct_min, ct_max); + int miredValue = kelvinToMired(((DecimalType) command).intValue()); + newLightState.ct = constrainToRange(miredValue, ctMin, ctMax); if (currentOn != null && !currentOn) { // sending new color temperature is only allowed when light is on @@ -240,7 +277,23 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (r.getResponseCode() == 403) { return null; } else if (r.getResponseCode() == 200) { - return gson.fromJson(r.getBody(), LightMessage.class); + LightMessage lightMessage = gson.fromJson(r.getBody(), LightMessage.class); + if (lightMessage != null && needsPropertyUpdate) { + // if we did not receive an ctmin/ctmax, then we probably don't need it + needsPropertyUpdate = false; + + if (lightMessage.ctmin != null && lightMessage.ctmax != null) { + Map properties = new HashMap<>(thing.getProperties()); + properties.put(PROPERTY_CT_MAX, + Integer.toString(Util.constrainToRange(lightMessage.ctmax, ZCL_CT_MIN, ZCL_CT_MAX))); + properties.put(PROPERTY_CT_MIN, + Integer.toString(Util.constrainToRange(lightMessage.ctmin, ZCL_CT_MIN, ZCL_CT_MAX))); + + logger.warn("properties new {}", properties); + updateProperties(properties); + } + } + return lightMessage; } else { throw new IllegalStateException("Unknown status code " + r.getResponseCode() + " for full state request"); } @@ -291,7 +344,7 @@ private void valueUpdated(String channelId, LightState newState) { break; case CHANNEL_COLOR_TEMPERATURE: Integer ct = newState.ct; - if (ct != null && ct >= ct_min && ct <= ct_max) { + if (ct != null && ct >= ctMin && ct <= ctMax) { updateState(channelId, new DecimalType(miredToKelvin(ct))); } break; diff --git a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java index 0366f51c4f271..ffb6e877a5cb6 100644 --- a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java +++ b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java @@ -12,15 +12,20 @@ */ package org.openhab.binding.deconz; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.MockitoAnnotations.initMocks; import static org.openhab.binding.deconz.internal.BindingConstants.*; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.library.types.DecimalType; import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingUID; @@ -32,6 +37,7 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.Mockito; +import org.openhab.binding.deconz.internal.StateDescriptionProvider; import org.openhab.binding.deconz.internal.dto.LightMessage; import org.openhab.binding.deconz.internal.handler.LightThingHandler; import org.openhab.binding.deconz.internal.types.LightType; @@ -54,6 +60,9 @@ public class LightsTest { @Mock private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback; + @Mock + private @NonNullByDefault({}) StateDescriptionProvider stateDescriptionProvider; + @Before public void initialize() { initMocks(this); @@ -76,7 +85,7 @@ public void colorTemperatureLightUpdateTest() throws IOException { Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID) .withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()) .withChannel(ChannelBuilder.create(channelUID_ct, "Number").build()).build(); - LightThingHandler lightThingHandler = new LightThingHandler(light, gson); + LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider); lightThingHandler.setCallback(thingHandlerCallback); lightThingHandler.messageReceived("", lightMessage); @@ -84,6 +93,32 @@ public void colorTemperatureLightUpdateTest() throws IOException { Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_ct), eq(new DecimalType("2500"))); } + @Test + public void colorTemperatureLightStateDescriptionProviderTest() { + ThingUID thingUID = new ThingUID("deconz", "light"); + ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS); + ChannelUID channelUID_ct = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE); + + Map properties = new HashMap<>(); + properties.put(PROPERTY_CT_MAX, "500"); + properties.put(PROPERTY_CT_MIN, "200"); + + Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID).withProperties(properties) + .withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()) + .withChannel(ChannelBuilder.create(channelUID_ct, "Number").build()).build(); + LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider) { + // avoid warning when initializing + @Override + public @Nullable Bridge getBridge() { + return null; + } + }; + + lightThingHandler.initialize(); + + Mockito.verify(stateDescriptionProvider).setDescription(eq(channelUID_ct), any()); + } + @Test public void dimmableLightUpdateTest() throws IOException { LightMessage lightMessage = DeconzTest.getObjectFromJson("dimmable.json", LightMessage.class, gson); @@ -94,7 +129,7 @@ public void dimmableLightUpdateTest() throws IOException { Thing light = ThingBuilder.create(THING_TYPE_DIMMABLE_LIGHT, thingUID) .withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()).build(); - LightThingHandler lightThingHandler = new LightThingHandler(light, gson); + LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider); lightThingHandler.setCallback(thingHandlerCallback); lightThingHandler.messageReceived("", lightMessage); @@ -111,7 +146,7 @@ public void windowCoveringUpdateTest() throws IOException { Thing light = ThingBuilder.create(THING_TYPE_WINDOW_COVERING, thingUID) .withChannel(ChannelBuilder.create(channelUID_pos, "Rollershutter").build()).build(); - LightThingHandler lightThingHandler = new LightThingHandler(light, gson); + LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider); lightThingHandler.setCallback(thingHandlerCallback); lightThingHandler.messageReceived("", lightMessage); From 25671d4cf270e7f168bf055fda584c5ac8112d61 Mon Sep 17 00:00:00 2001 From: Markus Michels Date: Wed, 8 Jul 2020 07:06:10 +0200 Subject: [PATCH 41/85] [gree] Initial contribution (#7504) * re-factoring WIP Signed-off-by: Markus Michels --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.gree/.classpath | 32 + bundles/org.openhab.binding.gree/.project | 23 + bundles/org.openhab.binding.gree/NOTICE | 13 + bundles/org.openhab.binding.gree/README.md | 141 +++++ bundles/org.openhab.binding.gree/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../gree/internal/GreeBindingConstants.java | 162 +++++ .../gree/internal/GreeConfiguration.java | 33 ++ .../binding/gree/internal/GreeCryptoUtil.java | 75 +++ .../binding/gree/internal/GreeException.java | 115 ++++ .../gree/internal/GreeHandlerFactory.java | 67 +++ .../internal/GreeTranslationProvider.java | 73 +++ .../internal/discovery/GreeDeviceFinder.java | 166 ++++++ .../discovery/GreeDiscoveryService.java | 129 ++++ .../internal/gson/GreeBindRequestPackDTO.java | 26 + .../internal/gson/GreeBindResponseDTO.java | 33 ++ .../gson/GreeBindResponsePackDTO.java | 27 + .../internal/gson/GreeExecResponseDTO.java | 34 ++ .../gson/GreeExecResponsePackDTO.java | 30 + .../gson/GreeExecuteCommandPackDTO.java | 28 + .../gree/internal/gson/GreeReqStatusDTO.java | 30 + .../internal/gson/GreeReqStatusPackDTO.java | 28 + .../gree/internal/gson/GreeRequestDTO.java | 30 + .../internal/gson/GreeScanReponsePackDTO.java | 37 ++ .../internal/gson/GreeScanRequestDTO.java | 24 + .../internal/gson/GreeScanResponseDTO.java | 31 + .../internal/gson/GreeStatusResponseDTO.java | 34 ++ .../gson/GreeStatusResponsePackDTO.java | 42 ++ .../gree/internal/handler/GreeAirDevice.java | 512 ++++++++++++++++ .../gree/internal/handler/GreeHandler.java | 552 ++++++++++++++++++ .../resources/ESH-INF/binding/binding.xml | 10 + .../resources/ESH-INF/i18n/gree.properties | 91 +++ .../resources/ESH-INF/i18n/gree_DE.properties | 90 +++ .../resources/ESH-INF/thing/thing-types.xml | 154 +++++ bundles/pom.xml | 1 + 37 files changed, 2905 insertions(+) create mode 100644 bundles/org.openhab.binding.gree/.classpath create mode 100644 bundles/org.openhab.binding.gree/.project create mode 100644 bundles/org.openhab.binding.gree/NOTICE create mode 100644 bundles/org.openhab.binding.gree/README.md create mode 100644 bundles/org.openhab.binding.gree/pom.xml create mode 100644 bundles/org.openhab.binding.gree/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeBindingConstants.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeConfiguration.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeCryptoUtil.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeException.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeHandlerFactory.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeTranslationProvider.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDeviceFinder.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDiscoveryService.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindRequestPackDTO.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindResponseDTO.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindResponsePackDTO.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecResponseDTO.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecResponsePackDTO.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecuteCommandPackDTO.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeReqStatusDTO.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeReqStatusPackDTO.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeRequestDTO.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanReponsePackDTO.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanRequestDTO.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanResponseDTO.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeStatusResponseDTO.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeStatusResponsePackDTO.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeAirDevice.java create mode 100644 bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java create mode 100644 bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/i18n/gree.properties create mode 100644 bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/i18n/gree_DE.properties create mode 100644 bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index 0d66331a9d6fb..8f1f1b98f59a4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -70,6 +70,7 @@ /bundles/org.openhab.binding.gardena/ @gerrieg /bundles/org.openhab.binding.globalcache/ @mhilbush /bundles/org.openhab.binding.gpstracker/ @gbicskei +/bundles/org.openhab.binding.gree/ @markus7017 /bundles/org.openhab.binding.groheondus/ @FlorianSW /bundles/org.openhab.binding.harmonyhub/ @digitaldan /bundles/org.openhab.binding.hdanywhere/ @kgoderis diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 54e1f2e6cdf73..aaf42e9c9a9e9 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -344,6 +344,11 @@ org.openhab.binding.gpstracker ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.gree + ${project.version} + org.openhab.addons.bundles org.openhab.binding.groheondus diff --git a/bundles/org.openhab.binding.gree/.classpath b/bundles/org.openhab.binding.gree/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.binding.gree/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.gree/.project b/bundles/org.openhab.binding.gree/.project new file mode 100644 index 0000000000000..5e843714895b6 --- /dev/null +++ b/bundles/org.openhab.binding.gree/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.gree + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.gree/NOTICE b/bundles/org.openhab.binding.gree/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.gree/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.gree/README.md b/bundles/org.openhab.binding.gree/README.md new file mode 100644 index 0000000000000..7f9d7af8a5ef7 --- /dev/null +++ b/bundles/org.openhab.binding.gree/README.md @@ -0,0 +1,141 @@ +# GREE Binding + +This binding integrates GREE Air Conditioners. + +Note: The GREE Air Conditioner must already be setup on the WiFi network and must have a fixed IP Address. + +## Supported Things + +This binding supports one Thing type `airconditioner`. + +## Discovery + +Once the GREE is on the network (WiFi active) it could be discovery automatically. +An IP broadcast message is sent and every responding unit gets added to the Inbox. + +## Binding Configuration + +No binding configuration is required. + +## Thing Configuration + +| Channel Name | Type | Description | +|------------------|------------|-----------------------------------------------------------------------------------------------| +| ipAddress | IP Address | IP address of the unit. | +| broadcastAddress | IP Address | Broadcast address being used for discovery, usually derived from the IP interface address. | +| refresh | Integer | Refresh interval in seconds for polling the device status. | + +The Air Conditioner's IP address is mandatory, all other parameters are optional. +If the broadcast is not set (default) it will be derived from openHAB's network setting (PaperUI:Configuration:System:Network Settings). +Only change this if you have a good reason to. + +## Channels + +The following channels are supported for fans: + +| Channel Name | Item Type | Description | +|---------------|-----------|---------------------------------------------------------------------------------------------------| +| power | Switch | Power on/off the Air Conditioner | +| mode | String | Sets the operating mode of the Air Conditioner | +| | | Mode can be one of auto/cool/eco/dry/fan/heat or on/off | +| | | Check the Air Conditioner's operating manual for supported modes. | +| temperature | Number:Temperature | Sets the desired room temperature | +| air | Switch | Set on/off the Air Conditioner's Air function if applicable to the Air Conditioner model | +| dry | Switch | Set on/off the Air Conditioner's Dry function if applicable to the Air Conditioner model | +| health | Switch | Set on/off the Air Conditioner's Health function if applicable to the Air Conditioner model | +| turbo | Switch | Set on/off the Air Conditioner's Turbo Mode. | +| quiet | String | Set Quiet Mode: off/auto/quiet | +| swingUpDown | Number | Sets the vertical (up..down) swing action on the Air Conditioner, | +| | | OFF: 0, Full Swing: 1, Up: 2, MidUp: 3, Mid: 4, Mid Down: 5, Down : 6 | +| swingLeftRight| Number | Sets the horizontal (left..right) swing action on the Air Conditioner | +| | | OFF: 0, Full Swing: 1, Left: 2, Mid Left: 3, Mid: 4, Mid Right: 5, Right : 6 | +| windspeed | Number | Sets the fan speed on the Air conditioner Auto:0, Low:1, MidLow:2, Mid:3, MidHigh:4, High:5 | +| | | The number of speeds depends on the Air Conditioner model. | +| powersave | Switch | Set on/off the Air Conditioner's Power Saving function if applicable to the Air Conditioner model | +| light | Switch | Enable/disable the front display on the Air Conditioner if applicable to the Air Conditioner model| +| | | Full Swing: 1, Up: 2, MidUp: 3, Mid: 4, Mid Down: 5, Down : 6 | + + +When changing mode, the air conditioner will be turned on unless "off" is selected. + +## Full Example + +**Things** + +``` +Thing gree:airconditioner:a1234561 [ ipAddress="192.168.1.111", refresh=2 ] +``` + +**Items** + +``` +Switch AirconPower { channel="gree:airconditioner:a1234561:power" } +Number AirconMode { channel="gree:airconditioner:a1234561:mode" } +Switch AirconTurbo { channel="gree:airconditioner:a1234561:turbo" } +Switch AirconLight { channel="gree:airconditioner:a1234561:light" } +Number AirconTemp "Temperature [%.1f °C]" {channel="gree:airconditioner:a1234561:temperature" } +Number AirconSwingVertical { channel="gree:airconditioner:a1234561:swingUpDown" } +Number AirconSwingHorizontal { channel="gree:airconditioner:a1234561:swingLeftRight" } +Number AirconFanSpeed { channel="gree:airconditioner:a1234561:windspeed" } +Switch AirconAir { channel="gree:airconditioner:a1234561:air" } +Switch AirconDry { channel="gree:airconditioner:a1234561:dry" } +Switch AirconHealth { channel="gree:airconditioner:a1234561:health" } +Switch AirconPowerSaving { channel="gree:airconditioner:a1234561:powersave" } +``` + +**Sitemap** + +This is an example of how to set up your sitemap. + +``` +Frame label="Controls" +{ + Switch item=AirconMode label="Mode" mappings=["auto"="Auto", "cool"="Cool", "eco"="Eco", "dry"="Dry", "fan"="Fan", "turbo"="Turbo", "heat"="Heat", "on"="ON", "off"="OFF"] + Setpoint item=AirconTemp label="Set temperature" icon=temperature minValue=16 maxValue=30 step=1 +} +Frame label="Fan Speed" +{ + Switch item=AirconFanSpeed label="Fan Speed" mappings=[0="Auto", 1="Low", 2="Medium Low", 3="Medium", 4="Medium High", 5="High"] icon=fan +} +Frame label="Fan-Swing Direction" +{ + Switch item=AirconSwingVertical label="Direction V" mappings=[0="Off", 1="Full", 2="Up", 3="Mid-up", 4="Mid", 5="Mid-low", 6="Down"] icon=flow + Switch item=AirconSwingHorizontal label="Direction H" mappings=[0="Off", 1="Full", 2="Left", 3="Mid-left", 4="Mid", 5="Mid-right", 6="Right"] icon=flow +} +Frame label="Options" +{ + Switch item=AirconTurbo label="Turbo" icon=fan + Switch item=AirconLight label="Light" icon=light + Switch item=AirconAir label="Air" icon=flow + Switch item=AirconDry label="Dry" icon=rain + Switch item=AirconHealth label="Health" icon=smiley + Switch item=AirconPowerSaving label="Power Saving" icon=poweroutlet +} +``` + +**Example** + +This example shows how to make a GREE Air Conditioner controllable by Google HA (A/C mode + temperature) + +**Items** + +``` +Group Gree_Modechannel "Gree" { ga="Thermostat" } // allows mapping for Google Home Assistent +Switch GreeAirConditioner_Power "Aircon" {channel="gree:airconditioner:a1234561:power", ga="Switch"} +Number GreeAirConditioner_Mode "Aircon Mode" {channel="gree:airconditioner:a1234561:mode", ga="thermostatMode"} +Number GreeAirConditioner_Temp "Aircon Temperature" {channel="gree:airconditioner:a1234561:temperature} +Switch GreeAirConditioner_Lightl "Light" {channel="gree:airconditioner:a1234561:light"} +``` + +**Rules** + +``` +rule "Mode changed" +when + Item GreeAirConditioner_Mode changed +then + if(GreeAirConditioner_Mode.state == "cool" ) { + logInfo("A/C", "Cooling has be turned on") + } +end +``` diff --git a/bundles/org.openhab.binding.gree/pom.xml b/bundles/org.openhab.binding.gree/pom.xml new file mode 100644 index 0000000000000..8e0e96b29b4c9 --- /dev/null +++ b/bundles/org.openhab.binding.gree/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.7-SNAPSHOT + + + org.openhab.binding.gree + + openHAB Add-ons :: Bundles :: Gree Binding + + diff --git a/bundles/org.openhab.binding.gree/src/main/feature/feature.xml b/bundles/org.openhab.binding.gree/src/main/feature/feature.xml new file mode 100644 index 0000000000000..3257cf303ac34 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.gree/${project.version} + + diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeBindingConstants.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeBindingConstants.java new file mode 100644 index 0000000000000..dab4521858562 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeBindingConstants.java @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal; + +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The {@link GreeBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author John Cunha - Initial contribution + * @author Markus Michels - Refactoring, adapted to OH 2.5x + */ +@NonNullByDefault +public class GreeBindingConstants { + + public static final String BINDING_ID = "gree"; + + public static final ThingTypeUID THING_TYPE_GREEAIRCON = new ThingTypeUID(BINDING_ID, "airconditioner"); + public static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_GREEAIRCON); + + // List of all Thing Type UIDs + public static final ThingTypeUID GREE_THING_TYPE = new ThingTypeUID(BINDING_ID, "airconditioner"); + + // Thing configuration items + public static final String PROPERTY_IP = "ipAddress"; + public static final String PROPERTY_BROADCAST = "broadcastAddress"; + + // List of all Channel ids + public static final String POWER_CHANNEL = "power"; + public static final String MODE_CHANNEL = "mode"; + public static final String TURBO_CHANNEL = "turbo"; + public static final String LIGHT_CHANNEL = "light"; + public static final String TEMP_CHANNEL = "temperature"; + public static final String SWINGUD_CHANNEL = "swingUpDown"; + public static final String SWINGLR_CHANNEL = "swingLeftRight"; + public static final String WINDSPEED_CHANNEL = "windspeed"; + public static final String QUIET_CHANNEL = "quiet"; + public static final String AIR_CHANNEL = "air"; + public static final String DRY_CHANNEL = "dry"; + public static final String HEALTH_CHANNEL = "health"; + public static final String PWRSAV_CHANNEL = "powersave"; + + // Mode channel + public static final String MODE_AUTO = "auto"; + public static final String MODE_COOL = "cool"; + public static final String MODE_DRY = "dry"; + public static final String MODE_FAN = "fan"; + public static final String MODE_FAN2 = "fan-only"; + public static final String MODE_HEAT = "heat"; + public static final String MODE_ECO = "eco"; + public static final String MODE_ON = "on"; + public static final String MODE_OFF = "off"; + public static final int GREE_MODE_AUTO = 0; + public static final int GREE_MODE_COOL = 1; + public static final int GREE_MODE_DRY = 2; + public static final int GREE_MODE_FAN = 3; + public static final int GREE_MODE_HEAT = 4; + + // Quiet channel + public static final String QUIET_OFF = "off"; + public static final String QUIET_AUTO = "auto"; + public static final String QUIET_QUIET = "quiet"; + public static final int GREE_QUIET_OFF = 0; + public static final int GREE_QUIET_AUTO = 1; + public static final int GREE_QUIET_QUIET = 2; + + // UDPPort used to communicate using UDP with GREE Airconditioners. . + public static final String VENDOR_GREE = "gree"; + public static final int GREE_PORT = 7000; + + public static final String GREE_CID = "app"; + public static final String GREE_CMDT_BIND = "bind"; + public static final String GREE_CMDT_SCAN = "scan"; + public static final String GREE_CMDT_STATUS = "status"; + public static final String GREE_CMDT_CMD = "cmd"; + public static final String GREE_CMDT_PACK = "pack"; + + public static final String GREE_CMD_OPT_NAME = "name"; // unit name + public static final String GREE_CMD_OPT_HOST = "host"; // remote host (cloud) + + /* + * Note : Values can be: + * "Pow": Power (0 or 1) + * "Mod": Mode: Auto: 0, Cool: 1, Dry: 2, Fan: 3, Heat: 4 + * "SetTem": Requested Temperature + * "WdSpd": Fan Speed : Low:1, Medium Low:2, Medium :3, Medium High :4, High :5 + * "Air": Air Mode Enabled + * "Blo": Dry + * "Health": Health + * "SwhSlp": Sleep + * "SlpMod": ??? + * "Lig": Light On + * "SwingLfRig": Swing Left Right + * "SwUpDn": Swing Up Down: // Ceiling:0, Upwards : 10, Downwards : 11, Full range : 1 + * "Quiet": Quiet mode + * "Tur": Turbo + * "StHt": 0, + * "TemUn": Temperature unit, 0 for Celsius, 1 for Fahrenheit + * "HeatCoolType" + * "TemRec": (0 or 1), Send with SetTem, when TemUn==1, distinguishes between upper and lower integer Fahrenheit + * temp + * "SvSt": Power Saving + */ + public static final String GREE_PROP_POWER = "Pow"; + public static final String GREE_PROP_MODE = "Mod"; + public static final String GREE_PROP_SWINGUPDOWN = "SwUpDn"; + public static final String GREE_PROP_SWINGLEFTRIGHT = "SwingLfRig"; + public static final String GREE_PROP_WINDSPEED = "WdSpd"; + public static final String GREE_PROP_AIR = "Air"; + public static final String GREE_PROP_DRY = "Blo"; + public static final String GREE_PROP_TURBO = "Tur"; + public static final String GREE_PROP_QUIET = "Quiet"; + public static final String GREE_PROP_NOISE = "NoiseSet"; + public static final String GREE_PROP_LIGHT = "Lig"; + public static final String GREE_PROP_HEALTH = "Health"; + public static final String GREE_PROP_SLEEP = "SwhSlp"; + public static final String GREE_PROP_SLEEPMODE = "SlpMod"; + public static final String GREE_PROP_PWR_SAVING = "SvSt"; + public static final String GREE_PROP_SETTEMP = "SetTem"; + public static final String GREE_PROP_TEMPUNIT = "TemUn"; + public static final String GREE_PROP_TEMPREC = "TemRec"; + public static final String GREE_PROP_HEAT = "StHt"; + public static final String GREE_PROP_HEATCOOL = "HeatCoolType"; + public static final String GREE_PROP_NOISESET = "NoiseSet"; + + // Temperatur types and min/max ranges + public static final int TEMP_UNIT_CELSIUS = 0; + public static final int TEMP_UNIT_FAHRENHEIT = 1; + public static final int TEMP_MIN_C = 16; + public static final int TEMP_MAX_C = 30; + public static final int TEMP_MIN_F = 61; + public static final int TEMP_MAX_F = 86; + public static final int TEMP_HALFSTEP_NO = 0; + public static final int TEMP_HALFSTEP_YES = 1; + + /* + * The timeout for the Datagram socket used to communicate with Gree Airconditioners. + * This is particularly important when scanning for devices because this will effectively + * be the amount of time spent scanning. + */ + public static final int DATAGRAM_SOCKET_TIMEOUT = 5000; // regular read timeout + public static final int DISCOVERY_TIMEOUT_MS = 7000; // do not change!! + public static final int MAX_SCAN_CYCLES = 3; + public static final int REFRESH_INTERVAL_SEC = 5; + + public static final int DIGITS_TEMP = 1; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeConfiguration.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeConfiguration.java new file mode 100644 index 0000000000000..5f5c0baa09840 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeConfiguration.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GreeConfiguration} class contains fields mapping thing configuration parameters. + * + * @author John Cunha - Initial contribution + * @author Markus Michels - Refactoring, adapted to OH 2.5x + */ +@NonNullByDefault +public class GreeConfiguration { + public String ipAddress = ""; + public String broadcastAddress = ""; + public int refresh = 60; + + @Override + public String toString() { + return "Config: ipAddress=" + ipAddress + ", broadcastAddress=" + broadcastAddress + ", refresh=" + refresh; + } +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeCryptoUtil.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeCryptoUtil.java new file mode 100644 index 0000000000000..2f8f9f90b0d3c --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeCryptoUtil.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal; + +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The CryptoUtil class provides functionality for encrypting and decrypting + * messages sent to and from the Air Conditioner + * + * @author John Cunha - Initial contribution + * @author Markus Michels - Refactoring, adapted to OH 2.5x + */ +@NonNullByDefault +public class GreeCryptoUtil { + private static final String AES_KEY = "a3K8Bx%2r8Y7#xDh"; + + public static byte[] getAESGeneralKeyByteArray() { + return AES_KEY.getBytes(StandardCharsets.UTF_8); + } + + public static String decryptPack(byte[] keyarray, String message) throws GreeException { + try { + Key key = new SecretKeySpec(keyarray, "AES"); + Base64.Decoder decoder = Base64.getDecoder(); + byte[] imageByte = decoder.decode(message); + + Cipher aesCipher = Cipher.getInstance("AES"); + aesCipher.init(Cipher.DECRYPT_MODE, key); + byte[] bytePlainText = aesCipher.doFinal(imageByte); + + return new String(bytePlainText, StandardCharsets.UTF_8); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException | InvalidKeyException + | IllegalBlockSizeException ex) { + throw new GreeException("Decryption of recieved data failed", ex); + } + } + + public static String encryptPack(byte[] keyarray, String message) throws GreeException { + try { + Key key = new SecretKeySpec(keyarray, "AES"); + Cipher aesCipher = Cipher.getInstance("AES"); + aesCipher.init(Cipher.ENCRYPT_MODE, key); + byte[] bytePlainText = aesCipher.doFinal(message.getBytes()); + + Base64.Encoder newencoder = Base64.getEncoder(); + return new String(newencoder.encode(bytePlainText), StandardCharsets.UTF_8); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException | InvalidKeyException + | IllegalBlockSizeException ex) { + throw new GreeException("Unable to encrypt outbound data", ex); + } + } +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeException.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeException.java new file mode 100644 index 0000000000000..73e350a0805b9 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeException.java @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal; + +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.text.MessageFormat; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonSyntaxException; + +/** + * {@link GreeException} implements a binding specific exception class. This allows to unity exception handling on the + * higher levels, but still carrying the exception, which caused the problem. + * + * @author Markus Michels - Initial Contribution + */ +@NonNullByDefault +public class GreeException extends Exception { + private static final long serialVersionUID = -2337258558995287405L; + private static String EX_NONE = "none"; + + public GreeException(Exception exception) { + super(exception); + } + + public GreeException(String message) { + super(message); + } + + public GreeException(String message, Exception exception) { + super(message, exception); + } + + @Override + public String getMessage() { + return isEmpty() ? "" : nonNullString(super.getMessage()); + } + + @Override + public String toString() { + String message = nonNullString(super.getMessage()); + String cause = getCauseClass().toString(); + if (!isEmpty()) { + if (isUnknownHost()) { + String[] string = message.split(": "); // java.net.UnknownHostException: api.rach.io + message = MessageFormat.format("Unable to connect to {0} (Unknown host / Network down / Low signal)", + string[1]); + } else if (isMalformedURL()) { + message = "Invalid URL"; + } else if (isTimeout()) { + message = "Device unreachable or API Timeout"; + } else { + message = MessageFormat.format("{0} ({1})", message, cause); + } + } else { + message = getMessage(); + } + return message; + } + + public boolean isApiException() { + return getCauseClass() == GreeException.class; + } + + public boolean isTimeout() { + Class extype = !isEmpty() ? getCauseClass() : null; + return (extype != null) && ((extype == SocketTimeoutException.class) || (extype == TimeoutException.class) + || (extype == ExecutionException.class) || (extype == InterruptedException.class) + || getMessage().toLowerCase().contains("timeout")); + } + + public boolean isUnknownHost() { + return getCauseClass() == MalformedURLException.class; + } + + public boolean isMalformedURL() { + return getCauseClass() == UnknownHostException.class; + } + + public boolean IsJSONException() { + return getCauseClass() == JsonSyntaxException.class; + } + + private boolean isEmpty() { + return nonNullString(super.getMessage()).equals(EX_NONE); + } + + private static String nonNullString(@Nullable String s) { + return s != null ? s : ""; + } + + private Class getCauseClass() { + Throwable cause = getCause(); + if (getCause() != null) { + return cause.getClass(); + } + return GreeException.class; + } +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeHandlerFactory.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeHandlerFactory.java new file mode 100644 index 0000000000000..981ef8dc92227 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeHandlerFactory.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal; + +import static org.openhab.binding.gree.internal.GreeBindingConstants.*; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.net.NetworkAddressService; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.openhab.binding.gree.internal.discovery.GreeDeviceFinder; +import org.openhab.binding.gree.internal.handler.GreeHandler; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link GreeHandlerFactory} is responsible for creating things and thing handlers. + * + * @author John Cunha - Initial contribution + * @author Markus Michels - Refactoring, adapted to OH 2.5x + */ +@NonNullByDefault +@Component(configurationPid = "binding." + BINDING_ID, service = ThingHandlerFactory.class) +public class GreeHandlerFactory extends BaseThingHandlerFactory { + private final GreeTranslationProvider messages; + private final GreeDeviceFinder deviceFinder; + + @Activate + public GreeHandlerFactory(@Reference NetworkAddressService networkAddressService, + @Reference GreeDeviceFinder deviceFinder, @Reference GreeTranslationProvider translationProvider, + ComponentContext componentContext, Map configProperties) { + super.activate(componentContext); + this.messages = translationProvider; + this.deviceFinder = deviceFinder; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + if (THING_TYPE_GREEAIRCON.equals(thing.getThingTypeUID())) { + return new GreeHandler(thing, messages, deviceFinder); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeTranslationProvider.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeTranslationProvider.java new file mode 100644 index 0000000000000..265d1e9b8b836 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeTranslationProvider.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal; + +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.i18n.LocaleProvider; +import org.eclipse.smarthome.core.i18n.TranslationProvider; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * {@link GreeTranslationProvider} provides i18n message lookup + * + * @author Markus Michels - Initial contribution + */ +@NonNullByDefault +@Component(service = GreeTranslationProvider.class, immediate = true, configurationPid = "localization.gree") +public class GreeTranslationProvider { + + private final Bundle bundle; + private final TranslationProvider i18nProvider; + private final LocaleProvider localeProvider; + + @Activate + public GreeTranslationProvider(@Reference TranslationProvider i18nProvider, + @Reference LocaleProvider localeProvider) { + this.bundle = FrameworkUtil.getBundle(this.getClass()); + this.i18nProvider = i18nProvider; + this.localeProvider = localeProvider; + } + + public GreeTranslationProvider(final GreeTranslationProvider other) { + this.bundle = other.bundle; + this.i18nProvider = other.i18nProvider; + this.localeProvider = other.localeProvider; + } + + public String get(String key, @Nullable Object... arguments) { + return getText(key.contains("@text/") ? key : "message." + key, arguments); + } + + public String getText(String key, @Nullable Object... arguments) { + try { + Locale locale = localeProvider.getLocale(); + String message = i18nProvider.getText(bundle, key, getDefaultText(key), locale, arguments); + if (message != null) { + return message; + } + } catch (IllegalArgumentException e) { + } + return "Unable to load message for key " + key; + } + + public @Nullable String getDefaultText(String key) { + return i18nProvider.getText(bundle, key, key, Locale.ENGLISH); + } +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDeviceFinder.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDeviceFinder.java new file mode 100644 index 0000000000000..3d4100dcb036d --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDeviceFinder.java @@ -0,0 +1,166 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.discovery; + +import static org.openhab.binding.gree.internal.GreeBindingConstants.*; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.gree.internal.GreeCryptoUtil; +import org.openhab.binding.gree.internal.GreeException; +import org.openhab.binding.gree.internal.gson.GreeScanReponsePackDTO; +import org.openhab.binding.gree.internal.gson.GreeScanRequestDTO; +import org.openhab.binding.gree.internal.gson.GreeScanResponseDTO; +import org.openhab.binding.gree.internal.handler.GreeAirDevice; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; + +/** + * The GreeDeviceFinder provides functionality for searching for GREE Airconditioners on the network and keeping a list + * of found devices. + * + * @author John Cunha - Initial contribution + * @author Markus Michels - Refactoring, adapted to OH 2.5x + */ +@NonNullByDefault +@Component(service = GreeDeviceFinder.class, immediate = true, configurationPid = "devicefinder.gree") +public class GreeDeviceFinder { + private final Logger logger = LoggerFactory.getLogger(GreeDeviceFinder.class); + private static final Gson gson = (new GsonBuilder()).create(); + + protected final InetAddress ipAddress = InetAddress.getLoopbackAddress(); + protected Map deviceTable = new HashMap<>(); + + @Activate + public GreeDeviceFinder() { + } + + public void scan(DatagramSocket clientSocket, String broadcastAddress, boolean scanNetwork) throws GreeException { + InetAddress ipAddress; + try { + ipAddress = InetAddress.getByName(broadcastAddress); + } catch (UnknownHostException e) { + throw new GreeException("Unknown host or invalid IP address", e); + } + try { + byte[] sendData = new byte[1024]; + byte[] receiveData = new byte[1024]; + + // Send the Scan message + GreeScanRequestDTO scanGson = new GreeScanRequestDTO(); + scanGson.t = GREE_CMDT_SCAN; + String scanReq = gson.toJson(scanGson); + sendData = scanReq.getBytes(StandardCharsets.UTF_8); + logger.debug("Sending scan packet to {}", ipAddress.getHostAddress()); + clientSocket.setSoTimeout(DISCOVERY_TIMEOUT_MS); + DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, ipAddress, DISCOVERY_TIMEOUT_MS); + clientSocket.send(sendPacket); + + // Loop for respnses from devices until we get a timeout. + int retries = scanNetwork ? MAX_SCAN_CYCLES : 1; + while ((retries > 0)) { + // Receive a response + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); + try { + clientSocket.receive(receivePacket); + InetAddress remoteAddress = receivePacket.getAddress(); + int remotePort = receivePacket.getPort(); + + // Read the response + GreeScanResponseDTO scanResponseGson = fromJson(receivePacket, GreeScanResponseDTO.class); + + // If there was no pack, ignore the response + if (scanResponseGson.pack == null) { + logger.debug("Invalid packet format, ignore"); + retries--; + continue; + } + + // Decrypt message - a a GreeException is thrown when something went wrong + String decryptedMsg = scanResponseGson.decryptedPack = GreeCryptoUtil + .decryptPack(GreeCryptoUtil.getAESGeneralKeyByteArray(), scanResponseGson.pack); + logger.debug("Response received from address {}: {}", remoteAddress.getHostAddress(), decryptedMsg); + + // Create the JSON to hold the response values + scanResponseGson.packJson = gson.fromJson(decryptedMsg, GreeScanReponsePackDTO.class); + + // Now make sure the device is reported as a Gree device + if (scanResponseGson.packJson.brand.equalsIgnoreCase("gree")) { + // Create a new GreeDevice + logger.debug("Discovered device at {}:{}", remoteAddress.getHostAddress(), remotePort); + GreeAirDevice newDevice = new GreeAirDevice(remoteAddress, remotePort, scanResponseGson); + addDevice(newDevice); + } else { + logger.debug("Unit discovered, but brand is not GREE"); + } + } catch (SocketTimeoutException e) { + return; + } catch (IOException | JsonSyntaxException e) { + retries--; + if (retries == 0) { + throw new GreeException("Exception on device scan", e); + } + } + } + } catch (IOException e) { + throw new GreeException("I/O exception during device scan", e); + } + } + + private T fromJson(DatagramPacket packet, Class classOfT) { + String json = new String(packet.getData(), StandardCharsets.UTF_8).replace("\\u0000", "").trim(); + return gson.fromJson(json, classOfT); + } + + public void addDevice(GreeAirDevice newDevice) { + deviceTable.put(newDevice.getId(), newDevice); + } + + public GreeAirDevice getDevice(String id) { + return deviceTable.get(id); + } + + public Map getDevices() { + return deviceTable; + } + + public @Nullable GreeAirDevice getDeviceByIPAddress(String ipAddress) { + for (GreeAirDevice currDevice : deviceTable.values()) { + if (currDevice.getAddress().getHostAddress().equals(ipAddress)) { + return currDevice; + } + } + return null; + } + + public int getScannedDeviceCount() { + return deviceTable.size(); + } + +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDiscoveryService.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDiscoveryService.java new file mode 100644 index 0000000000000..ca11d61332ac8 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDiscoveryService.java @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.gree.internal.discovery; + +import static org.openhab.binding.gree.internal.GreeBindingConstants.*; + +import java.net.DatagramSocket; +import java.net.SocketException; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; +import org.eclipse.smarthome.config.discovery.DiscoveryResult; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.net.NetworkAddressService; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.openhab.binding.gree.internal.GreeException; +import org.openhab.binding.gree.internal.GreeTranslationProvider; +import org.openhab.binding.gree.internal.handler.GreeAirDevice; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link GreeDiscoveryService} implements the device discovery service. UDP broadtcast ius used to find the devices on + * the local subnet. + * + * @author Markus Michels - Initial contribution + * + */ +@NonNullByDefault +@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.gree") +public class GreeDiscoveryService extends AbstractDiscoveryService { + private static final int TIMEOUT_SEC = 10; + private final Logger logger = LoggerFactory.getLogger(GreeDiscoveryService.class); + private final GreeDeviceFinder deviceFinder; + private final GreeTranslationProvider messages; + private final String broadcastAddress; + + @Activate + public GreeDiscoveryService(@Reference GreeDeviceFinder deviceFinder, + @Reference NetworkAddressService networkAddressService, + @Reference GreeTranslationProvider translationProvider, + @Nullable Map configProperties) { + super(SUPPORTED_THING_TYPES_UIDS, TIMEOUT_SEC); + this.messages = translationProvider; + this.deviceFinder = deviceFinder; + String ip = networkAddressService.getConfiguredBroadcastAddress(); + broadcastAddress = ip != null ? ip : ""; + activate(configProperties); + } + + @Override + @Modified + protected void modified(@Nullable Map configProperties) { + super.modified(configProperties); + } + + @Override + protected void startBackgroundDiscovery() { + // It's very unusual that a new unit gets installed frequently so we run the discovery once when the binding is + // started, but not frequently + scheduler.execute(this::startScan); + } + + @Override + protected void stopBackgroundDiscovery() { + stopScan(); + } + + @Override + protected void startScan() { + try (DatagramSocket clientSocket = new DatagramSocket()) { + deviceFinder.scan(clientSocket, broadcastAddress, true); + + int count = deviceFinder.getScannedDeviceCount(); + logger.debug("{}", messages.get("discovery.result", count)); + if (count > 0) { + logger.debug("Adding uinits to Inbox"); + createResult(deviceFinder.getDevices()); + } + } catch (GreeException e) { + logger.info("Discovery: {}", messages.get("discovery.exception", e.getMessage())); + } catch (SocketException | RuntimeException e) { + logger.warn("Discovery: {}", messages.get("discovery.exception", "RuntimeException"), e); + } + } + + public void createResult(Map deviceList) { + for (GreeAirDevice device : deviceList.values()) { + String ipAddress = device.getAddress().getHostAddress(); + logger.debug("{}", messages.get("discovery.newunit", device.getName(), ipAddress, device.getId())); + Map properties = new HashMap<>(); + properties.put(Thing.PROPERTY_VENDOR, device.getVendor()); + properties.put(Thing.PROPERTY_MODEL_ID, device.getModel()); + properties.put(Thing.PROPERTY_MAC_ADDRESS, device.getId()); + properties.put(PROPERTY_IP, ipAddress); + properties.put(PROPERTY_BROADCAST, broadcastAddress); + ThingUID thingUID = new ThingUID(THING_TYPE_GREEAIRCON, device.getId()); + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withProperties(properties) + .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).withLabel(device.getName()).build(); + thingDiscovered(result); + } + } + + @Override + public void deactivate() { + removeOlderResults(getTimestampOfLastScan()); + super.deactivate(); + } +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindRequestPackDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindRequestPackDTO.java new file mode 100644 index 0000000000000..4d06ba43b5b0d --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindRequestPackDTO.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.gson; + +/** + * + * The GreeBindRequestPack4Gson class is used by Gson to hold values to be send to + * the Air Conditioner during Binding + * + * @author John Cunha - Initial contribution + */ +public class GreeBindRequestPackDTO { + public String mac = null; + public String t = null; + public int uid = 0; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindResponseDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindResponseDTO.java new file mode 100644 index 0000000000000..cf1590980b05b --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindResponseDTO.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.gson; + +/** + * + * The GreeBindResponse4Gson class is used by Gson to hold values returned from + * the Air Conditioner during Binding + * + * @author John Cunha - Initial contribution + */ +public class GreeBindResponseDTO { + + public String t = null; + public int i = 0; + public int uid = 0; + public String cid = null; + public String tcid = null; + public String pack = null; + + public transient String decryptedPack = null; + public transient GreeBindResponsePackDTO packJson = null; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindResponsePackDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindResponsePackDTO.java new file mode 100644 index 0000000000000..af490bb44ea2b --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindResponsePackDTO.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.gson; + +/** + * + * The GreeBindResponsePack4Gson class is used by Gson to hold values returned from + * the Air Conditioner during Binding + * + * @author John Cunha - Initial contribution + */ +public class GreeBindResponsePackDTO { + public String t = null; + public String mac = null; + public String key = null; + public int r = 0; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecResponseDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecResponseDTO.java new file mode 100644 index 0000000000000..5fc79e2e4a17a --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecResponseDTO.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.gson; + +/** + * + * The GreeExecResponse4Gson class is used by Gson to hold values returned from + * the Air Conditioner during requests for Execution of Commands to the + * Air Conditioner. + * + * @author John Cunha - Initial contribution + */ +public class GreeExecResponseDTO { + + public String t = null; + public int i = 0; + public int uid = 0; + public String cid = null; + public String tcid = null; + public String pack = null; + + public transient String decryptedPack = null; + public transient GreeExecResponsePackDTO packJson = null; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecResponsePackDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecResponsePackDTO.java new file mode 100644 index 0000000000000..9d6833071505c --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecResponsePackDTO.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.gson; + +/** + * + * The GreeExecResponsePack4Gson class is used by Gson to hold values returned from + * the Air Conditioner during requests for Execution of Commands to the + * Air Conditioner. + * + * @author John Cunha - Initial contribution + */ +public class GreeExecResponsePackDTO { + public String t = null; + public String mac = null; + public int r = 0; + public String[] opt = null; + public Integer[] p = null; + public Integer[] val = null; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecuteCommandPackDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecuteCommandPackDTO.java new file mode 100644 index 0000000000000..eb591bd8cb153 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecuteCommandPackDTO.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.gson; + +/** + * + * The GreeExecuteCommandPack4Gson class is used by Gson to hold values to be send to + * the Air Conditioner during requests for Execution of Commands to the + * Air Conditioner. + * + * @author John Cunha - Initial contribution + */ +public class GreeExecuteCommandPackDTO { + + public String[] opt = null; + public Integer[] p = null; + public String t = null; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeReqStatusDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeReqStatusDTO.java new file mode 100644 index 0000000000000..0a0edf2cdd460 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeReqStatusDTO.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.gson; + +/** + * + * The GreeReqStatus4Gson class is used by Gson to hold values to be send to + * the Air Conditioner during requests for Status Updates to the + * Air Conditioner. + * + * @author John Cunha - Initial contribution + */ +public class GreeReqStatusDTO { + public String cid = null; + public int i = 0; + public String t = null; + public int uid = 0; + public String pack = null; + public String tcid = null; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeReqStatusPackDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeReqStatusPackDTO.java new file mode 100644 index 0000000000000..1f47d5c096926 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeReqStatusPackDTO.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.gson; + +/** + * + * The GreeReqStatusPack4Gson class is used by Gson to hold values to be send to + * the Air Conditioner during requests for Status Updates to the + * Air Conditioner. + * + * @author John Cunha - Initial contribution + */ +public class GreeReqStatusPackDTO { + + public String t = null; + public String[] cols = null; + public String mac = null; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeRequestDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeRequestDTO.java new file mode 100644 index 0000000000000..b8b493b88e75f --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeRequestDTO.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.gson; + +/** + * + * The GreeBindRequest4Gson class is used by Gson to hold values to be send to + * the Air Conditioner during Binding + * + * @author John Cunha - Initial contribution + */ +public class GreeRequestDTO { + + public int uid = 0; + public String t = null; + public int i = 0; + public String pack = null; + public String cid = null; + public String tcid = null; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanReponsePackDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanReponsePackDTO.java new file mode 100644 index 0000000000000..e2b6d5c411c7c --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanReponsePackDTO.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.gson; + +/** + * + * The GreeScanReponsePack4Gson class is used by Gson to hold values returned by + * the Air Conditioner during Scan Requests to the Air Conditioner. + * + * @author John Cunha - Initial contribution + */ +public class GreeScanReponsePackDTO { + + public String t = null; + public String cid = null; + public String bc = null; + public String brand = null; + public String catalog = null; + public String mac = null; + public String mid = null; + public String model = null; + public String name = null; + public String series = null; + public String vender = null; + public String ver = null; + public int lock = 0; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanRequestDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanRequestDTO.java new file mode 100644 index 0000000000000..cc6fec59ac9bf --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanRequestDTO.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.gson; + +/** + * + * The GreeScanRequest4Gson class is used by Gson to hold values sent to + * the Air Conditioner during Scan Requests to the Air Conditioner. + * + * @author John Cunha - Initial contribution + */ +public class GreeScanRequestDTO { + public String t = null; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanResponseDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanResponseDTO.java new file mode 100644 index 0000000000000..1b58834c28669 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanResponseDTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.gson; + +/** + * + * The GreeScanResponse4Gson class is used by Gson to hold values returned by + * the Air Conditioner during Scan Requests to the Air Conditioner. + * + * @author John Cunha - Initial contribution + */ +public class GreeScanResponseDTO { + public String t = null; + public int i = 0; + public int uid = 0; + public String cid = null; + public String tcid = null; + public String pack = null; + public transient String decryptedPack = null; + public transient GreeScanReponsePackDTO packJson = null; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeStatusResponseDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeStatusResponseDTO.java new file mode 100644 index 0000000000000..baa012866861b --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeStatusResponseDTO.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.gson; + +/** + * + * The GreeStatusResponse4Gson class is used by Gson to hold values returned from + * the Air Conditioner during requests for Status Updates to the + * Air Conditioner. + * + * @author John Cunha - Initial contribution + */ +public class GreeStatusResponseDTO { + + public String t = null; + public int i = 0; + public int uid = 0; + public String cid = null; + public String tcid = null; + public String pack = null; + + public transient String decryptedPack = null; + public transient GreeStatusResponsePackDTO packJson = null; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeStatusResponsePackDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeStatusResponsePackDTO.java new file mode 100644 index 0000000000000..4cbdf08484ea4 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeStatusResponsePackDTO.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.gson; + +/** + * + * The GreeStatusResponsePack4Gson class is used by Gson to hold values returned from + * the Air Conditioner during requests for Status Updates to the + * Air Conditioner. + * + * @author John Cunha - Initial contribution + */ +public class GreeStatusResponsePackDTO { + + public GreeStatusResponsePackDTO(GreeStatusResponsePackDTO other) { + if (other.cols != null) { + cols = new String[other.cols.length]; + dat = new Integer[other.dat.length]; + System.arraycopy(other.cols, 0, cols, 0, other.cols.length); + System.arraycopy(other.dat, 0, dat, 0, other.dat.length); + } else { + cols = new String[0]; + dat = new Integer[0]; + } + } + + public String t = null; + public String mac = null; + public int r = 0; + public String[] cols = null; + public Integer[] dat = null; +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeAirDevice.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeAirDevice.java new file mode 100644 index 0000000000000..d6a66b65a3bb0 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeAirDevice.java @@ -0,0 +1,512 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.handler; + +import static org.openhab.binding.gree.internal.GreeBindingConstants.*; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.openhab.binding.gree.internal.GreeCryptoUtil; +import org.openhab.binding.gree.internal.GreeException; +import org.openhab.binding.gree.internal.gson.GreeBindRequestPackDTO; +import org.openhab.binding.gree.internal.gson.GreeBindResponseDTO; +import org.openhab.binding.gree.internal.gson.GreeBindResponsePackDTO; +import org.openhab.binding.gree.internal.gson.GreeExecResponseDTO; +import org.openhab.binding.gree.internal.gson.GreeExecResponsePackDTO; +import org.openhab.binding.gree.internal.gson.GreeExecuteCommandPackDTO; +import org.openhab.binding.gree.internal.gson.GreeReqStatusPackDTO; +import org.openhab.binding.gree.internal.gson.GreeRequestDTO; +import org.openhab.binding.gree.internal.gson.GreeScanResponseDTO; +import org.openhab.binding.gree.internal.gson.GreeStatusResponseDTO; +import org.openhab.binding.gree.internal.gson.GreeStatusResponsePackDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +/** + * The GreeDevice object repesents a Gree Airconditioner and provides + * device specific attributes as well a the functionality for the Air Conditioner + * + * @author John Cunha - Initial contribution + * @author Markus Michels - Refactoring, adapted to OH 2.5x + */ +@NonNullByDefault +public class GreeAirDevice { + private final Logger logger = LoggerFactory.getLogger(GreeAirDevice.class); + private final static Gson gson = new Gson(); + private boolean isBound = false; + private final InetAddress ipAddress; + private int port = 0; + private String encKey = ""; + private Optional scanResponseGson = Optional.empty(); + private Optional statusResponseGson = Optional.empty(); + private Optional prevStatusResponsePackGson = Optional.empty(); + + public GreeAirDevice() { + ipAddress = InetAddress.getLoopbackAddress(); + } + + public GreeAirDevice(InetAddress ipAddress, int port, GreeScanResponseDTO scanResponse) { + this.ipAddress = ipAddress; + this.port = port; + this.scanResponseGson = Optional.of(scanResponse); + } + + public void getDeviceStatus(DatagramSocket clientSocket) throws GreeException { + + if (!isBound) { + throw new GreeException("Device not bound"); + } + try { + // Set the values in the HashMap + ArrayList columns = new ArrayList<>(); + columns.add(GREE_PROP_POWER); + columns.add(GREE_PROP_MODE); + columns.add(GREE_PROP_SETTEMP); + columns.add(GREE_PROP_WINDSPEED); + columns.add(GREE_PROP_AIR); + columns.add(GREE_PROP_DRY); + columns.add(GREE_PROP_HEALTH); + columns.add(GREE_PROP_SLEEP); + columns.add(GREE_PROP_LIGHT); + columns.add(GREE_PROP_SWINGLEFTRIGHT); + columns.add(GREE_PROP_SWINGUPDOWN); + columns.add(GREE_PROP_QUIET); + columns.add(GREE_PROP_TURBO); + columns.add(GREE_PROP_TEMPUNIT); + columns.add(GREE_PROP_HEAT); + columns.add(GREE_PROP_HEATCOOL); + columns.add(GREE_PROP_TEMPREC); + columns.add(GREE_PROP_PWR_SAVING); + columns.add(GREE_PROP_NOISESET); + + // Convert the parameter map values to arrays + String[] colArray = columns.toArray(new String[0]); + + // Prep the Command Request pack + GreeReqStatusPackDTO reqStatusPackGson = new GreeReqStatusPackDTO(); + reqStatusPackGson.t = GREE_CMDT_STATUS; + reqStatusPackGson.cols = colArray; + reqStatusPackGson.mac = getId(); + String reqStatusPackStr = gson.toJson(reqStatusPackGson); + + // Encrypt and send the Status Request pack + String encryptedStatusReqPacket = GreeCryptoUtil.encryptPack(getKey(), reqStatusPackStr); + DatagramPacket sendPacket = createPackRequest(0, + new String(encryptedStatusReqPacket.getBytes(), StandardCharsets.UTF_8)); + clientSocket.send(sendPacket); + + // Keep a copy of the old response to be used to check if values have changed + // If first time running, there will not be a previous GreeStatusResponsePack4Gson + if (statusResponseGson.isPresent() && statusResponseGson.get().packJson != null) { + prevStatusResponsePackGson = Optional + .of(new GreeStatusResponsePackDTO(statusResponseGson.get().packJson)); + } + + // Read the response, create the JSON to hold the response values + GreeStatusResponseDTO resp = receiveResponse(clientSocket, GreeStatusResponseDTO.class); + resp.decryptedPack = GreeCryptoUtil.decryptPack(getKey(), resp.pack); + logger.debug("Response from device: {}", resp.decryptedPack); + resp.packJson = gson.fromJson(resp.decryptedPack, GreeStatusResponsePackDTO.class); + + // save the results + statusResponseGson = Optional.of(resp); + updateTempFtoC(); + } catch (IOException | JsonSyntaxException e) { + throw new GreeException("I/O exception while updating status", e); + } catch (RuntimeException e) { + logger.debug("Exception", e); + String json = statusResponseGson.map(r -> r.packJson.toString()).orElse("n/a"); + throw new GreeException("Exception while updating status, JSON=" + json, e); + } + } + + public void bindWithDevice(DatagramSocket clientSocket) throws GreeException { + try { + // Prep the Binding Request pack + GreeBindRequestPackDTO bindReqPackGson = new GreeBindRequestPackDTO(); + bindReqPackGson.mac = getId(); + bindReqPackGson.t = GREE_CMDT_BIND; + bindReqPackGson.uid = 0; + String bindReqPackStr = gson.toJson(bindReqPackGson); + + // Encrypt and send the Binding Request pack + String encryptedBindReqPacket = GreeCryptoUtil.encryptPack(GreeCryptoUtil.getAESGeneralKeyByteArray(), + bindReqPackStr); + DatagramPacket sendPacket = createPackRequest(1, encryptedBindReqPacket); + clientSocket.send(sendPacket); + + // Recieve a response, create the JSON to hold the response values + GreeBindResponseDTO resp = receiveResponse(clientSocket, GreeBindResponseDTO.class); + resp.decryptedPack = GreeCryptoUtil.decryptPack(GreeCryptoUtil.getAESGeneralKeyByteArray(), resp.pack); + resp.packJson = gson.fromJson(resp.decryptedPack, GreeBindResponsePackDTO.class); + + // Now set the key and flag to indicate the bind was succesful + encKey = resp.packJson.key; + + // save the outcome + isBound = true; + } catch (IOException | JsonSyntaxException e) { + throw new GreeException("Unable to bind to device", e); + } + } + + public void setDevicePower(DatagramSocket clientSocket, int value) throws GreeException { + setCommandValue(clientSocket, GREE_PROP_POWER, value); + } + + public void setDeviceMode(DatagramSocket clientSocket, int value) throws GreeException { + if ((value < 0 || value > 4)) { + throw new GreeException("Device mode out of range!"); + } + setCommandValue(clientSocket, GREE_PROP_MODE, value); + } + + public void setDeviceSwingUpDown(DatagramSocket clientSocket, int value) throws GreeException { + // Only values 0,1,2,3,4,5,6,10,11 allowed + if ((value < 0 || value > 11) || (value > 6 && value < 10)) { + throw new GreeException("SwingUpDown value out of range!"); + } + setCommandValue(clientSocket, GREE_PROP_SWINGUPDOWN, value); + } + + public void setDeviceSwingLeftRight(DatagramSocket clientSocket, int value) throws GreeException { + setCommandValue(clientSocket, GREE_PROP_SWINGLEFTRIGHT, value, 0, 6); + } + + /** + * Only allow this to happen if this device has been bound and values are valid + * Possible values are : + * 0 : Auto + * 1 : Low + * 2 : Medium Low + * 3 : Medium + * 4 : Medium High + * 5 : High + */ + public void setDeviceWindspeed(DatagramSocket clientSocket, int value) throws GreeException { + if (value < 0 || value > 5) { + throw new GreeException("Value out of range!"); + } + + HashMap parameters = new HashMap<>(); + parameters.put(GREE_PROP_WINDSPEED, value); + parameters.put(GREE_PROP_QUIET, 0); + parameters.put(GREE_PROP_TURBO, 0); + parameters.put(GREE_PROP_NOISE, 0); + executeCommand(clientSocket, parameters); + } + + public void setDeviceTurbo(DatagramSocket clientSocket, int value) throws GreeException { + setCommandValue(clientSocket, GREE_PROP_TURBO, value, 0, 1); + } + + public void setQuietMode(DatagramSocket clientSocket, int value) throws GreeException { + setCommandValue(clientSocket, GREE_PROP_QUIET, value, 0, 2); + } + + public int getDeviceTurbo() { + return getIntStatusVal(GREE_PROP_TURBO); + } + + public void setDeviceLight(DatagramSocket clientSocket, int value) throws GreeException { + setCommandValue(clientSocket, GREE_PROP_LIGHT, value); + } + + /** + * @param value set temperature in degrees Celsius or Fahrenheit + */ + public void setDeviceTempSet(DatagramSocket clientSocket, QuantityType temp) throws GreeException { + // If commanding Fahrenheit set halfStep to 1 or 0 to tell the A/C which F integer + // temperature to use as celsius alone is ambigious + double newVal = temp.doubleValue(); + int CorF = temp.getUnit() == SIUnits.CELSIUS ? TEMP_UNIT_CELSIUS : TEMP_UNIT_FAHRENHEIT; // 0=Celsius, + // 1=Fahrenheit + if (((CorF == TEMP_UNIT_CELSIUS) && (newVal < TEMP_MIN_C || newVal > TEMP_MAX_C)) + || ((CorF == TEMP_UNIT_FAHRENHEIT) && (newVal < TEMP_MIN_F || newVal > TEMP_MAX_F))) { + throw new IllegalArgumentException("Temp Value out of Range"); + } + + // Default for Celsius + int outVal = (int) newVal; + int halfStep = TEMP_HALFSTEP_NO; // for whatever reason halfStep is not supported for Celsius + + // If value argument is degrees F, convert Fahrenheit to Celsius, + // SetTem input to A/C always in Celsius despite passing in 1 to TemUn + // ******************TempRec TemSet Mapping for setting Fahrenheit**************************** + // F = [68...86] + // C = [20.0, 20.5, 21.1, 21.6, 22.2, 22.7, 23.3, 23.8, 24.4, 25.0, 25.5, 26.1, 26.6, 27.2, 27.7, 28.3, + // 28.8, 29.4, 30.0] + // + // TemSet = [20..30] or [68..86] + // TemRec = value - (value) > 0 ? 1 : 1 -> when xx.5 is request xx will become TemSet and halfStep the indicator + // for "half on top of TemSet" + // ******************TempRec TemSet Mapping for setting Fahrenheit**************************** + // subtract the float version - the int version to get the fractional difference + // if the difference is positive set halfStep to 1, negative to 0 + if (CorF == TEMP_UNIT_FAHRENHEIT) { // If Fahrenheit, + halfStep = newVal - outVal > 0 ? TEMP_HALFSTEP_YES : TEMP_HALFSTEP_NO; + } + logger.debug("Converted temp from {}{} to temp={}, halfStep={}, unit={})", newVal, temp.getUnit(), outVal, + halfStep, CorF == TEMP_UNIT_CELSIUS ? "C" : "F"); + + // Set the values in the HashMap + HashMap parameters = new HashMap<>(); + parameters.put(GREE_PROP_TEMPUNIT, CorF); + parameters.put(GREE_PROP_SETTEMP, outVal); + parameters.put(GREE_PROP_TEMPREC, halfStep); + executeCommand(clientSocket, parameters); + } + + public void setDeviceAir(DatagramSocket clientSocket, int value) throws GreeException { + setCommandValue(clientSocket, GREE_PROP_AIR, value); + } + + public void setDeviceDry(DatagramSocket clientSocket, int value) throws GreeException { + setCommandValue(clientSocket, GREE_PROP_DRY, value); + } + + public void setDeviceHealth(DatagramSocket clientSocket, int value) throws GreeException { + setCommandValue(clientSocket, GREE_PROP_HEALTH, value); + } + + public void setDevicePwrSaving(DatagramSocket clientSocket, int value) throws GreeException { + // Set the values in the HashMap + HashMap parameters = new HashMap<>(); + parameters.put(GREE_PROP_PWR_SAVING, value); + parameters.put(GREE_PROP_WINDSPEED, 0); + parameters.put(GREE_PROP_QUIET, 0); + parameters.put(GREE_PROP_TURBO, 0); + parameters.put(GREE_PROP_SLEEP, 0); + parameters.put(GREE_PROP_SLEEPMODE, 0); + executeCommand(clientSocket, parameters); + } + + public int getIntStatusVal(String valueName) { + /* + * Note : Values can be: + * "Pow": Power (0 or 1) + * "Mod": Mode: Auto: 0, Cool: 1, Dry: 2, Fan: 3, Heat: 4 + * "SetTem": Requested Temperature + * "WdSpd": Fan Speed : Low:1, Medium Low:2, Medium :3, Medium High :4, High :5 + * "Air": Air Mode Enabled + * "Blo": Dry + * "Health": Health + * "SwhSlp": Sleep + * "SlpMod": ??? + * "Lig": Light On + * "SwingLfRig": Swing Left Right + * "SwUpDn": Swing Up Down: // Ceiling:0, Upwards : 10, Downwards : 11, Full range : 1 + * "Quiet": Quiet mode + * "Tur": Turbo + * "StHt": 0, + * "TemUn": Temperature unit, 0 for Celsius, 1 for Fahrenheit + * "HeatCoolType" + * "TemRec": (0 or 1), Send with SetTem, when TemUn==1, distinguishes between upper and lower integer Fahrenheit + * temp + * "SvSt": Power Saving + */ + // Find the valueName in the Returned Status object + if (isStatusAvailable()) { + List colList = Arrays.asList(statusResponseGson.get().packJson.cols); + List valList = Arrays.asList(statusResponseGson.get().packJson.dat); + int valueArrayposition = colList.indexOf(valueName); + if (valueArrayposition != -1) { + // get the Corresponding value + Integer value = valList.get(valueArrayposition); + return value; + } + } + + return -1; + } + + public boolean isStatusAvailable() { + return statusResponseGson.isPresent(); + } + + public boolean hasStatusValChanged(String valueName) throws GreeException { + if (!prevStatusResponsePackGson.isPresent()) { + return true; // update value if there is no previous one + } + // Find the valueName in the Current Status object + List currcolList = Arrays.asList(statusResponseGson.get().packJson.cols); + List currvalList = Arrays.asList(statusResponseGson.get().packJson.dat); + int currvalueArrayposition = currcolList.indexOf(valueName); + if (currvalueArrayposition == -1) { + throw new GreeException("Unable to decode device status"); + } + + // Find the valueName in the Previous Status object + List prevcolList = Arrays.asList(prevStatusResponsePackGson.get().cols); + List prevvalList = Arrays.asList(prevStatusResponsePackGson.get().dat); + int prevvalueArrayposition = prevcolList.indexOf(valueName); + if (prevvalueArrayposition == -1) { + throw new GreeException("Unable to get status value"); + } + + // Finally Compare the values + return currvalList.get(currvalueArrayposition) != prevvalList.get(prevvalueArrayposition); + } + + protected void executeCommand(DatagramSocket clientSocket, Map parameters) throws GreeException { + // Only allow this to happen if this device has been bound + if (!getIsBound()) { + throw new GreeException("Device is not bound!"); + } + + try { + // Convert the parameter map values to arrays + String[] keyArray = parameters.keySet().toArray(new String[0]); + Integer[] valueArray = parameters.values().toArray(new Integer[0]); + + // Prep the Command Request pack + GreeExecuteCommandPackDTO execCmdPackGson = new GreeExecuteCommandPackDTO(); + execCmdPackGson.opt = keyArray; + execCmdPackGson.p = valueArray; + execCmdPackGson.t = GREE_CMDT_CMD; + String execCmdPackStr = gson.toJson(execCmdPackGson); + + // Now encrypt and send the Command Request pack + String encryptedCommandReqPacket = GreeCryptoUtil.encryptPack(getKey(), execCmdPackStr); + DatagramPacket sendPacket = createPackRequest(0, encryptedCommandReqPacket); + clientSocket.send(sendPacket); + + // Receive and decode result + GreeExecResponseDTO execResponseGson = receiveResponse(clientSocket, GreeExecResponseDTO.class); + execResponseGson.decryptedPack = GreeCryptoUtil.decryptPack(getKey(), execResponseGson.pack); + + // Create the JSON to hold the response values + execResponseGson.packJson = gson.fromJson(execResponseGson.decryptedPack, GreeExecResponsePackDTO.class); + } catch (IOException | JsonSyntaxException e) { + throw new GreeException("Exception on command execution", e); + } + } + + private void setCommandValue(DatagramSocket clientSocket, String command, int value) throws GreeException { + executeCommand(clientSocket, Collections.singletonMap(command, value)); + } + + private void setCommandValue(DatagramSocket clientSocket, String command, int value, int min, int max) + throws GreeException { + if ((value < min) || (value > max)) { + throw new GreeException("Command value out of range!"); + } + executeCommand(clientSocket, Collections.singletonMap(command, value)); + } + + private DatagramPacket createPackRequest(int i, String pack) { + GreeRequestDTO request = new GreeRequestDTO(); + request.cid = GREE_CID; + request.i = i; + request.t = GREE_CMDT_PACK; + request.uid = 0; + request.tcid = getId(); + request.pack = pack; + byte[] sendData = gson.toJson(request).getBytes(StandardCharsets.UTF_8); + DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, ipAddress, port); + return sendPacket; + } + + private T receiveResponse(DatagramSocket clientSocket, Class classOfT) + throws IOException, JsonSyntaxException { + byte[] receiveData = new byte[1024]; + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); + clientSocket.receive(receivePacket); + String json = new String(receivePacket.getData(), StandardCharsets.UTF_8).replace("\\u0000", "").trim(); + return gson.fromJson(json, classOfT); + } + + private void updateTempFtoC() { + // Status message back from A/C always reports degrees C + // If using Fahrenheit, us SetTem, TemUn and TemRec to reconstruct the Fahrenheit temperature + // Get Celsius or Fahrenheit from status message + int CorF = getIntStatusVal(GREE_PROP_TEMPUNIT); + int newVal = getIntStatusVal(GREE_PROP_SETTEMP); + int halfStep = getIntStatusVal(GREE_PROP_TEMPREC); + + if ((CorF == -1) || (newVal == -1) || (halfStep == -1)) { + throw new IllegalArgumentException("SetTem,TemUn or TemRec is invalid, not performing conversion"); + } else if (CorF == 1) { // convert SetTem to Fahrenheit + // Find the valueName in the Returned Status object + String[] columns = statusResponseGson.get().packJson.cols; + Integer[] values = statusResponseGson.get().packJson.dat; + List colList = Arrays.asList(columns); + int valueArrayposition = colList.indexOf(GREE_PROP_SETTEMP); + if (valueArrayposition != -1) { + // convert Celsius to Fahrenheit, + // SetTem status returns degrees C regardless of TempUn setting + + // Perform the float Celsius to Fahrenheit conversion add or subtract 0.5 based on the value of TemRec + // (0 = -0.5, 1 = +0.5). Pass into a rounding function, this yeild the correct Fahrenheit Temperature to + // match A/C display + newVal = (int) (Math.round(((newVal * 9.0 / 5.0) + 32.0) + halfStep - 0.5)); + + // Update the status array with F temp, assume this is updating the array in situ + values[valueArrayposition] = newVal; + } + } + } + + public InetAddress getAddress() { + return ipAddress; + } + + public boolean getIsBound() { + return isBound; + } + + public byte[] getKey() { + return encKey.getBytes(StandardCharsets.UTF_8); + } + + public String getId() { + return scanResponseGson.isPresent() ? scanResponseGson.get().packJson.mac : ""; + } + + public String getName() { + return scanResponseGson.isPresent() ? scanResponseGson.get().packJson.name : ""; + } + + public String getVendor() { + return scanResponseGson.isPresent() + ? scanResponseGson.get().packJson.brand + " " + scanResponseGson.get().packJson.vender + : ""; + } + + public String getModel() { + return scanResponseGson.isPresent() + ? scanResponseGson.get().packJson.series + " " + scanResponseGson.get().packJson.model + : ""; + } + + public void setScanResponseGson(GreeScanResponseDTO gson) { + scanResponseGson = Optional.of(gson); + } +} diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java new file mode 100644 index 0000000000000..256abbf883266 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java @@ -0,0 +1,552 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.gree.internal.handler; + +import static org.openhab.binding.gree.internal.GreeBindingConstants.*; + +import java.io.IOException; +import java.math.BigDecimal; +import java.net.DatagramSocket; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.unit.ImperialUnits; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.gree.internal.GreeConfiguration; +import org.openhab.binding.gree.internal.GreeException; +import org.openhab.binding.gree.internal.GreeTranslationProvider; +import org.openhab.binding.gree.internal.discovery.GreeDeviceFinder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link GreeHandler} is responsible for handling commands, which are sent to one of the channels. + * + * @author John Cunha - Initial contribution + * @author Markus Michels - Refactoring, adapted to OH 2.5x + */ +@NonNullByDefault +public class GreeHandler extends BaseThingHandler { + private final Logger logger = LoggerFactory.getLogger(GreeHandler.class); + private final GreeTranslationProvider messages; + private final GreeDeviceFinder deviceFinder; + private final String thingId; + private GreeConfiguration config = new GreeConfiguration(); + private GreeAirDevice device = new GreeAirDevice(); + private Optional clientSocket = Optional.empty(); + private boolean forceRefresh = false; + + private @Nullable ScheduledFuture refreshTask; + private @Nullable Future initializeFuture; + private long lastRefreshTime = 0; + + public GreeHandler(Thing thing, GreeTranslationProvider messages, GreeDeviceFinder deviceFinder) { + super(thing); + this.messages = messages; + this.deviceFinder = deviceFinder; + this.thingId = getThing().getUID().getId(); + } + + @Override + public void initialize() { + config = getConfigAs(GreeConfiguration.class); + if (config.ipAddress.isEmpty() || (config.refresh < 0)) { + String message = messages.get("thinginit.invconf"); + logger.warn("{}: {}", thingId, message); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, message); + } + + // set the thing status to UNKNOWN temporarily and let the background task decide for the real status. + // the framework is then able to reuse the resources from the thing handler initialization. + updateStatus(ThingStatus.UNKNOWN); + + // Start the automatic refresh cycles + startAutomaticRefresh(); + initializeFuture = scheduler.submit(this::initializeThing); + } + + private void initializeThing() { + String message = ""; + try { + if (!clientSocket.isPresent()) { + clientSocket = Optional.of(new DatagramSocket()); + clientSocket.get().setSoTimeout(DATAGRAM_SOCKET_TIMEOUT); + } + // Find the GREE device + deviceFinder.scan(clientSocket.get(), config.ipAddress, false); + GreeAirDevice newDevice = deviceFinder.getDeviceByIPAddress(config.ipAddress); + if (newDevice != null) { + // Ok, our device responded, now let's Bind with it + device = newDevice; + device.bindWithDevice(clientSocket.get()); + if (device.getIsBound()) { + updateStatus(ThingStatus.ONLINE); + return; + } + } + + message = messages.get("thinginit.failed"); + logger.info("{}: {}", thingId, message); + } catch (GreeException e) { + logger.info("{}: {}", thingId, messages.get("thinginit.exception", e.getMessage())); + } catch (IOException e) { + logger.warn("{}: {}", thingId, messages.get("thinginit.exception", "I/O Error"), e); + } catch (RuntimeException e) { + logger.warn("{}: {}", thingId, messages.get("thinginit.exception", "RuntimeException"), e); + } + + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + // The thing is updated by the scheduled automatic refresh so do nothing here. + } else { + logger.debug("{}: Issue command {} to channe {}", thingId, command, channelUID.getIdWithoutGroup()); + String channelId = channelUID.getIdWithoutGroup(); + logger.debug("{}: Handle command {} for channel {}, command class {}", thingId, command, channelId, + command.getClass()); + try { + DatagramSocket socket = clientSocket.get(); + switch (channelId) { + case MODE_CHANNEL: + handleModeCommand(socket, command); + break; + case POWER_CHANNEL: + device.setDevicePower(socket, getOnOff(command)); + break; + case TURBO_CHANNEL: + device.setDeviceTurbo(socket, getOnOff(command)); + break; + case LIGHT_CHANNEL: + device.setDeviceLight(socket, getOnOff(command)); + break; + case TEMP_CHANNEL: + // Set value, read back effective one and update channel + // e.g. 22.5C will result in 22.0, because the AC doesn't support half-steps for C + device.setDeviceTempSet(socket, convertTemp(command)); + break; + case SWINGUD_CHANNEL: + device.setDeviceSwingUpDown(socket, getNumber(command)); + break; + case SWINGLR_CHANNEL: + device.setDeviceSwingLeftRight(socket, getNumber(command)); + break; + case WINDSPEED_CHANNEL: + device.setDeviceWindspeed(socket, getNumber(command)); + break; + case QUIET_CHANNEL: + handleQuietCommand(socket, command); + break; + case AIR_CHANNEL: + device.setDeviceAir(socket, getOnOff(command)); + break; + case DRY_CHANNEL: + device.setDeviceDry(socket, getOnOff(command)); + break; + case HEALTH_CHANNEL: + device.setDeviceHealth(socket, getOnOff(command)); + break; + case PWRSAV_CHANNEL: + device.setDevicePwrSaving(socket, getOnOff(command)); + break; + } + + // force refresh on next status refresh cycle + forceRefresh = true; + } catch (GreeException e) { + String message = logInfo("command.exception", command, channelId) + ": " + e.getMessage(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); + } catch (IllegalArgumentException e) { + logInfo("command.invarg", command, channelId); + } catch (RuntimeException e) { + logger.warn("{}: {}", thingId, messages.get("command.exception", command, channelId), e); + } + } + } + + private void handleModeCommand(DatagramSocket socket, Command command) throws GreeException { + int mode = -1; + String modeStr = ""; + boolean isNumber = false; + if (command instanceof DecimalType) { + // backward compatibility when channel was Number + mode = ((DecimalType) command).intValue(); + } else if (command instanceof OnOffType) { + // Switch + logger.debug("{}: Send Power-{}", thingId, command); + device.setDevicePower(socket, getOnOff(command)); + } else /* String */ { + modeStr = command.toString().toLowerCase(); + switch (modeStr) { + case MODE_AUTO: + mode = GREE_MODE_AUTO; + break; + case MODE_COOL: + mode = GREE_MODE_COOL; + break; + case MODE_HEAT: + mode = GREE_MODE_HEAT; + break; + case MODE_DRY: + mode = GREE_MODE_DRY; + break; + case MODE_FAN: + case MODE_FAN2: + mode = GREE_MODE_FAN; + break; + case MODE_ECO: + // power saving will be set after the uinit was turned on + mode = GREE_MODE_COOL; + break; + case MODE_ON: + case MODE_OFF: + logger.debug("{}: Turn unit {}", thingId, modeStr); + device.setDevicePower(socket, modeStr.equals(MODE_ON) ? 1 : 0); + return; + default: + // fallback: mode number, pass transparent + // if string is not parsable parseInt() throws an exception + mode = Integer.parseInt(modeStr); + isNumber = true; + break; + } + logger.debug("{}: Mode {} mapped to {}", thingId, modeStr, mode); + } + + if (mode == -1) { + throw new IllegalArgumentException("Invalid Mode selection"); + } + + // Turn on the unit if currently off + if (!isNumber && (device.getIntStatusVal(GREE_PROP_POWER) == 0)) { + logger.debug("{}: Send Auto-ON for mode {}", thingId, mode); + device.setDevicePower(socket, 1); + } + + // Select mode + logger.debug("{}: Select mode {}", thingId, mode); + device.setDeviceMode(socket, mode); + + // Check for secondary action + switch (modeStr) { + case MODE_ECO: + // Turn on power saving for eco mode + logger.debug("{}: Turn on Power-Saving", thingId); + device.setDevicePwrSaving(socket, 1); + break; + } + } + + private void handleQuietCommand(DatagramSocket socket, Command command) throws GreeException { + int mode = -1; + if (command instanceof DecimalType) { + mode = ((DecimalType) command).intValue(); + } else if (command instanceof StringType) { + switch (command.toString().toLowerCase()) { + case QUIET_OFF: + mode = GREE_QUIET_OFF; + break; + case QUIET_AUTO: + mode = GREE_QUIET_AUTO; + break; + case QUIET_QUIET: + mode = GREE_QUIET_QUIET; + break; + } + } + if (mode != -1) { + device.setQuietMode(socket, mode); + } else { + throw new IllegalArgumentException("Invalid QuietType"); + } + } + + private int getOnOff(Command command) { + if (command instanceof OnOffType) { + return command == OnOffType.ON ? 1 : 0; + } + if (command instanceof DecimalType) { + int value = ((DecimalType) command).intValue(); + if ((value == 0) || (value == 1)) { + return value; + } + } + throw new IllegalArgumentException("Invalid OnOffType"); + } + + private int getNumber(Command command) { + if (command instanceof DecimalType) { + return ((DecimalType) command).intValue(); + } + throw new IllegalArgumentException("Invalid Number type"); + } + + private QuantityType convertTemp(Command command) { + if (command instanceof DecimalType) { + // The Number alone doesn't specify the temp unit + // for this get current setting from the A/C unit + int unit = device.getIntStatusVal(GREE_PROP_TEMPUNIT); + return toQuantityType((DecimalType) command, DIGITS_TEMP, + unit == TEMP_UNIT_CELSIUS ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT); + } + if (command instanceof QuantityType) { + return (QuantityType) command; + } + throw new IllegalArgumentException("Invalud Temp type"); + } + + private void startAutomaticRefresh() { + Runnable refresher = () -> { + try { + // safeguard for multiple REFRESH commands + if (isMinimumRefreshTimeExceeded()) { + // Get the current status from the Airconditioner + + if (getThing().getStatus() == ThingStatus.OFFLINE) { + initializeThing(); + return; + } + + if (clientSocket.isPresent()) { + device.getDeviceStatus(clientSocket.get()); + logger.debug("{}: Executing automatic update of values", thingId); + List channels = getThing().getChannels(); + for (Channel channel : channels) { + publishChannel(channel.getUID()); + } + } + } + } catch (GreeException e) { + String subcode = ""; + if (e.getCause() != null) { + subcode = " (" + e.getCause().getMessage() + ")"; + } + String message = messages.get("update.exception", e.getMessage() + subcode); + if (getThing().getStatus() == ThingStatus.OFFLINE) { + logger.debug("{}: Thing still OFFLINE ({})", thingId, message); + } else { + if (!e.isTimeout()) { + logger.info("{}: {}", thingId, message); + } else { + logger.debug("{}: {}", thingId, message); + } + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); + } + } catch (RuntimeException e) { + String message = messages.get("update.exception", "RuntimeException"); + logger.warn("{}: {}", thingId, message, e); + } + }; + + if (refreshTask == null) { + refreshTask = scheduler.scheduleWithFixedDelay(refresher, 0, REFRESH_INTERVAL_SEC, TimeUnit.SECONDS); + logger.debug("{}: Automatic refresh started ({} second interval)", thingId, config.refresh); + forceRefresh = true; + } + } + + private boolean isMinimumRefreshTimeExceeded() { + long currentTime = Instant.now().toEpochMilli(); + long timeSinceLastRefresh = currentTime - lastRefreshTime; + if (!forceRefresh && (timeSinceLastRefresh < config.refresh * 1000)) { + return false; + } + lastRefreshTime = currentTime; + return true; + } + + private void publishChannel(ChannelUID channelUID) { + String channelID = channelUID.getId(); + try { + State state = null; + switch (channelUID.getIdWithoutGroup()) { + case POWER_CHANNEL: + state = updateOnOff(GREE_PROP_POWER); + break; + case MODE_CHANNEL: + state = updateMode(); + break; + case TURBO_CHANNEL: + state = updateOnOff(GREE_PROP_TURBO); + break; + case LIGHT_CHANNEL: + state = updateOnOff(GREE_PROP_LIGHT); + break; + case TEMP_CHANNEL: + state = updateTemp(); + break; + case SWINGUD_CHANNEL: + state = updateNumber(GREE_PROP_SWINGUPDOWN); + break; + case SWINGLR_CHANNEL: + state = updateNumber(GREE_PROP_SWINGLEFTRIGHT); + break; + case WINDSPEED_CHANNEL: + state = updateNumber(GREE_PROP_WINDSPEED); + break; + case QUIET_CHANNEL: + state = updateQuiet(); + break; + case AIR_CHANNEL: + state = updateOnOff(GREE_PROP_AIR); + break; + case DRY_CHANNEL: + state = updateOnOff(GREE_PROP_DRY); + break; + case HEALTH_CHANNEL: + state = updateOnOff(GREE_PROP_HEALTH); + break; + case PWRSAV_CHANNEL: + state = updateOnOff(GREE_PROP_PWR_SAVING); + break; + } + if (state != null) { + logger.debug("{}: Updating channel {} : {}", thingId, channelID, state); + updateState(channelID, state); + } + } catch (GreeException e) { + logger.info("{}: {}", thingId, messages.get("channel.exception", channelID, e.getMessage())); + } catch (RuntimeException e) { + logger.warn("{}: {}", thingId, messages.get("channel.exception", "RuntimeException"), e); + } + } + + private @Nullable State updateOnOff(final String valueName) throws GreeException { + if (device.hasStatusValChanged(valueName)) { + return device.getIntStatusVal(valueName) == 1 ? OnOffType.ON : OnOffType.OFF; + } + return null; + } + + private @Nullable State updateNumber(final String valueName) throws GreeException { + if (device.hasStatusValChanged(valueName)) { + return new DecimalType(device.getIntStatusVal(valueName)); + } + return null; + } + + private @Nullable State updateMode() throws GreeException { + if (device.hasStatusValChanged(GREE_PROP_MODE)) { + int mode = device.getIntStatusVal(GREE_PROP_MODE); + String modeStr = ""; + switch (mode) { + case GREE_MODE_AUTO: + modeStr = MODE_AUTO; + break; + case GREE_MODE_COOL: + boolean powerSave = device.getIntStatusVal(GREE_PROP_PWR_SAVING) == 1; + modeStr = !powerSave ? MODE_COOL : MODE_ECO; + break; + case GREE_MODE_DRY: + modeStr = MODE_DRY; + break; + case GREE_MODE_FAN: + modeStr = MODE_FAN; + break; + case GREE_MODE_HEAT: + modeStr = MODE_HEAT; + break; + default: + modeStr = String.valueOf(mode); + + } + if (!modeStr.isEmpty()) { + logger.debug("{}: Updading mode channel with {}/{}", thingId, mode, modeStr); + return new StringType(modeStr); + } + } + return null; + } + + private @Nullable State updateQuiet() throws GreeException { + if (device.hasStatusValChanged(GREE_PROP_QUIET)) { + switch (device.getIntStatusVal(GREE_PROP_QUIET)) { + case GREE_QUIET_OFF: + return new StringType(QUIET_OFF); + case GREE_QUIET_AUTO: + return new StringType(QUIET_AUTO); + case GREE_QUIET_QUIET: + return new StringType(QUIET_QUIET); + } + } + return null; + } + + private @Nullable State updateTemp() throws GreeException { + if (device.hasStatusValChanged(GREE_PROP_SETTEMP) || device.hasStatusValChanged(GREE_PROP_TEMPUNIT)) { + int unit = device.getIntStatusVal(GREE_PROP_TEMPUNIT); + return toQuantityType(device.getIntStatusVal(GREE_PROP_SETTEMP), DIGITS_TEMP, + unit == TEMP_UNIT_CELSIUS ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT); + } + return null; + } + + private String logInfo(String msgKey, Object... arg) { + String message = messages.get(msgKey, arg); + logger.info("{}: {}", thingId, message); + return message; + } + + public static QuantityType toQuantityType(Number value, int digits, Unit unit) { + BigDecimal bd = new BigDecimal(value.doubleValue()); + return new QuantityType<>(bd.setScale(digits, BigDecimal.ROUND_HALF_EVEN), unit); + } + + private void stopRefrestTask() { + forceRefresh = false; + if (refreshTask == null) { + return; + } + ScheduledFuture task = refreshTask; + if (task != null) { + task.cancel(true); + } + refreshTask = null; + } + + @Override + public void dispose() { + logger.debug("{}: Thing {} is disposing", thingId, thing.getUID()); + if (clientSocket.isPresent()) { + clientSocket.get().close(); + clientSocket = Optional.empty(); + } + stopRefrestTask(); + if (initializeFuture != null) { + initializeFuture.cancel(true); + } + } +} diff --git a/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..b5f8cb1e46eb5 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + GREE Binding + This is the binding for GREE air conditioners. + Markus Michels + + diff --git a/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/i18n/gree.properties b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/i18n/gree.properties new file mode 100644 index 0000000000000..7ec07822b5a85 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/i18n/gree.properties @@ -0,0 +1,91 @@ +# GREE Binding +binding.gree.name = GREE Binding +binding.gree.description = This binding integrates the GREE series of air conditioners + +# thing types +thing-type.gree.airconditioner.label = Air Conditioner +thing-type.gree.airconditioner.description = A GREE Air Conditioner with WiFi Module + +# thing type config description +thing-type.config.gree.airconditioner.ipAddress.label = IP Address +thing-type.config.gree.airconditioner.ipAddress.description = IP Address of the GREE unit. +thing-type.config.gree.airconditioner.broadcastAddress.label = Subnet Broadcast Address +thing-type.config.gree.airconditioner.broadcastAddress.description = Broadcast IP address of the local subnet. +thing-type.config.gree.airconditioner.refresh.label = Refresh Interval +thing-type.config.gree.airconditioner.refresh.description = Interval to query an update from the device. + + +# channel types +channel-type.gree.power.label = Power +channel-type.gree.power.description = Turn power on/off +channel-type.gree.mode.label = Unit Mode +channel-type.gree.mode.description = Operating mode of the Air Conditioner: auto/cool/eco/fan/dry/turbo or on/off +channel-type.gree.mode.state.option.auto = Auto +channel-type.gree.mode.state.option.cool = Cool +channel-type.gree.mode.state.option.eco = Eco +channel-type.gree.mode.state.option.dry = Dry +channel-type.gree.mode.state.option.fan = Fan +channel-type.gree.mode.state.option.turbo = Turbo +channel-type.gree.mode.state.option.heat = Heat +channel-type.gree.mode.state.option.on = ON +channel-type.gree.mode.state.option.off = OFF +channel-type.gree.air.label = Air Mode +channel-type.gree.air.description = Set on/off the Air Conditioner's Air function if applicable to the Air Conditioner model. +channel-type.gree.dry.label = Dry Mode +channel-type.gree.dry.description = Set on/off the Air Conditioner's Dry function if applicable to the Air Conditioner model. +channel-type.gree.turbo.label = Turbo Mode +channel-type.gree.turbo.description = Set on/off the Air Conditioner's Turbo mode. +channel-type.gree.temperature.label = Temperature +channel-type.gree.temperature.description = Sets the desired room temperature. +channel-type.gree.windspeed.label = Wind Speed +channel-type.gree.windspeed.description = Sets the fan speed on the Air conditioner: Auto:0, Low:1, MidLow:2, Mid:3, MidHigh:4, High:5. The number of speeds depends on the Air Conditioner model. +channel-type.gree.windspeed.state.option.0 = Auto +channel-type.gree.windspeed.state.option.1 = Low +channel-type.gree.windspeed.state.option.2 = Medium Low +channel-type.gree.windspeed.state.option.3 = Medium +channel-type.gree.windspeed.state.option.4 = Medium High +channel-type.gree.windspeed.state.option.5 = High +channel-type.gree.mode.label = Unit Mode +channel-type.gree.mode.description = Operating mode of the Air Conditioner: Auto: 0, Cool: 1, Dry: 2, Fan: 3, Heat: 4 +channel-type.gree.mode.state.option.auto = Auto +channel-type.gree.swingupdown.label = Vertical Swing Mode +channel-type.gree.swingupdown.description = Sets the vertical swing action on the Air Conditioner: 0=OFF, 1=Full Swing, 2=Up, 3=Mid-Up 4=Mid, 5=Mid-Down, 6=Down +channel-type.gree.swingupdown.option.0 = OFF +channel-type.gree.swingupdown.option.1 = Full Swing +channel-type.gree.swingupdown.option.2 = Up +channel-type.gree.swingupdown.option.3 = Mid-Up +channel-type.gree.swingupdown.option.4 = Mid +channel-type.gree.swingupdown.option.5 = Mid-Down +channel-type.gree.swingupdown.option.6 = Down +channel-type.gree.swingleftright.label = Horizontal Swing Mode +channel-type.gree.swingleftright.description = Sets the horizontal swing action on the Air Conditioner: 0=OFF, 1=Full Swing, 2=Left, 3=Mid-Left, 4=Mid, 5=Mid-Right, 6=Right +channel-type.gree.swingleftright.option.0 = OFF +channel-type.gree.swingleftright.option.1 = Full Swing +channel-type.gree.swingleftright.option.2 = Left +channel-type.gree.swingleftright.option.3 = Mid-Left +channel-type.gree.swingleftright.option.4 = Mid +channel-type.gree.swingleftright.option.5 = Mid-Right +channel-type.gree.swingleftright.option.6 = Right +channel-type.gree.quiet.label = Quiet Mode +channel-type.gree.quiet.description = Sets the quiet mode, 0=OFF, 1=Auto, 2=Quiet +channel-type.gree.quiet.state.option.off = OFF +channel-type.gree.quiet.state.option.auto = Auto +channel-type.gree.quiet.state.option.quiet = Quiet +channel-type.gree.powersave.label = Power Save +channel-type.gree.powersave.description = Set on/off the Air Conditioner's Power Saving function if applicable to the Air Conditioner model. +channel-type.gree.light.label = Light +channel-type.gree.light.description = Enable/disable the front display on the Air. +channel-type.gree.health.label = Health Mode +channel-type.gree.health.description = Set on/off the Air Conditioner's Health function if applicable to the Air Conditioner model. + +# User Messages +message.thinginit.failed = Unable to connect to air conditioner +message.thinginit.invconf = Invalid configuration data +message.thinginit.exception = Thing initialization failed: {0} +message.command.invarg = Invalid command value {}for channel {} +message.command.exception = Unable to execute command {0}for channel {1} +message.update.exception = Unable to perform auto-update: {0} +message.channel.exception = Unable to update channel {0} with {1} +message.discovery.result = {0} units discovered. +message.discovery.newunit = Device {0} discovered at {1}, MAC={2} +message.discovery.exception = Device Discovery failed: {0} diff --git a/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/i18n/gree_DE.properties b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/i18n/gree_DE.properties new file mode 100644 index 0000000000000..e93203ac2fe45 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/i18n/gree_DE.properties @@ -0,0 +1,90 @@ +# GREE Binding +binding.gree.name = GREE Binding +binding.gree.label = GREE Air Conditioner +binding.gree.description = Dieses Binding integriert Klimaanlagen der Marke GREE + +# thing types +thing-type.gree.airconditioner.label = GREE Klimaanlage +thing-type.gree.airconditioner.description = Eine GREE Klimaanlage mit WiFi Modul + +# thing type config description +thing-type.config.gree.airconditioner.ipAddress.label = IP Adresse +thing-type.config.gree.airconditioner.ipAddress.description = IP Adresse des GREE-Gertes. +thing-type.config.gree.airconditioner.broadcastAddress.label = IP Broadcast-Adresse +thing-type.config.gree.airconditioner.broadcastAddress.description = Broadcast IP Adresse des lokalen Subnetzes. +thing-type.config.gree.airconditioner.refresh.label = Aktualisierungsintervall +thing-type.config.gree.airconditioner.refresh.description = Intervall, in dem der Status des Gertes aktualisiert wird. + + +# channel types +channel-type.gree.power.label = Betrieb +channel-type.gree.power.description = Schaltet das Gert ein/aus. +channel-type.gree.mode.label = Betriebsmodus +channel-type.gree.mode.description = Betriebsmodus der Klimaanlage: auto/cool/eco/fan/dry/turbo or on/off +channel-type.gree.mode.state.option.auto = Auto +channel-type.gree.mode.state.option.cool = Khlen +channel-type.gree.mode.state.option.eco = Eco +channel-type.gree.mode.state.option.dry = Trocknen +channel-type.gree.mode.state.option.fan = Ventilator +channel-type.gree.mode.state.option.turbo = Turbo +channel-type.gree.mode.state.option.heat = Heizen +channel-type.gree.mode.state.option.on = Ein +channel-type.gree.mode.state.option.off = Aus +channel-type.gree.air.label = L¸ftung +channel-type.gree.air.description = Schaltet das Gert in den L¸ftermodus (keine Khlung). Verfgbarkeit ist abhngig vom Gertemodell. +channel-type.gree.dry.label = Trocknen +channel-type.gree.dry.description = Schaltet den Trocknungsmodus ein/aus. Verfgbarkeit ist abhngig vom Gertemodell. +channel-type.gree.turbo.label = Turbo +channel-type.gree.turbo.description = Schaltet den Turbomodus ein/aus. Verfgbarkeit ist abhngig vom Gertemodell. +channel-type.gree.temperature.label = Temperatur +channel-type.gree.temperature.description = Setzt die Zieltemperatur. +channel-type.gree.windspeed.label = Lftergeschwindigkeit +channel-type.gree.windspeed.description = Geschwindigkeit der Ventilation: 0:Auto, 1=niedrig, 2: langsam, 3: mittel, 4: schneller, 5: hoch. Verfgbarkeit der Geschwindigkeitsstufen ist abhngig vom Gertemodell. +channel-type.gree.windspeed.state.option.0 = Auto +channel-type.gree.windspeed.state.option.1 = Niedrig +channel-type.gree.windspeed.state.option.2 = Mittel +channel-type.gree.windspeed.state.option.3 = Schnell +channel-type.gree.windspeed.state.option.4 = Stark +channel-type.gree.windspeed.state.option.5 = Max +channel-type.gree.quiet.label = Leisemodus +channel-type.gree.quiet.description = Leisemodus whlen: 0=Aus, 1=Auto, 2=Leise +channel-type.gree.quiet.state.option.off = Aus +channel-type.gree.quiet.state.option.auto = Auto +channel-type.gree.quiet.state.option.quiet = Leise +channel-type.gree.swingupdown.label = Lamellenmodus +channel-type.gree.swingupdown.description = Auswahl des Lamellenmodus: 0=Aus, 1=Voller Flgel, 2=Hoch, 3=Mittelhoch, 3=Mitte, 5=Mitteltief, 6=Tief. Verfgbarkeit ist abhngig vom Gertemodell. +channel-type.gree.swingupdown.option.0 = Aus +channel-type.gree.swingupdown.option.1 = Voller Flgel +channel-type.gree.swingupdown.option.2 = Hoch +channel-type.gree.swingupdown.option.3 = Mittelhoch +channel-type.gree.swingupdown.option.4 = Mitte +channel-type.gree.swingupdown.option.5 = Mitteltief +channel-type.gree.swingupdown.option.6 = Tief +channel-type.gree.swingleftright.label = Lamellenmodus +channel-type.gree.swingleftright.description = Auswahl des Lamellenmodus: 0=Aus, 1=Voller Flgel, 2=Links, 3=Mitte-Links, 4=Mitte, 5=Mitte-Rechts, 6=Rechts. Verfgbarkeit ist abhngig vom Gertemodell. +channel-type.gree.swingleftright.option.0 = AUS +channel-type.gree.swingleftright.option.1 = Voller Flgel +channel-type.gree.swingleftright.option.2 = Links +channel-type.gree.swingleftright.option.3 = Mitte-Links +channel-type.gree.swingleftright.option.4 = Mitte +channel-type.gree.swingleftright.option.5 = Mitte-Rechts +channel-type.gree.swingleftright.option.6 = Rechts +channel-type.gree.powersave.label = Energiesparen +channel-type.gree.powersave.description = Aktivierung der Energiesparfunktion. Verfgbarkeit ist abhngig vom Gertemodell. +channel-type.gree.light.label = Kontrollleuchte +channel-type.gree.light.description = Die Beleuchtung an der Frontseite ein/ausschalten. +channel-type.gree.health.label = Betriebsbereitschaft +channel-type.gree.health.description = Zeigt die Betriebsbeschreitschaft des Gertes an. + +# User Messages +message.thinginit.failed = Klimaanlage nicht erreichbar +message.thinginit.invconf = Ungltiger Thing-Konfigurationswert +message.thinginit.exception = Initialisierung fehlgeschlagen: {0} +message.command.invarg = Ungltiger Befehlswert {0}fr Channel {1} +message.command.exception = Befehl {0}fr Channel {1} kann nichts ausgefhrt werden +message.update.exception = Status-Update fehlgeschlagen: {0} +message.channel.exception = Aktualisierung des Channels {0} mit dem Wert {1} ist fehlgeschlagen +message.discovery.result = {0} Gerte gefunden. +message.discovery.newunit = Gert {0} wurde mit IP-Adresse {1} erkannt (MAC={2}) +message.discovery.exception =Gerteerkennung fehlgeschlagen: {0} + diff --git a/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..1aa75875aad71 --- /dev/null +++ b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/thing/thing-types.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + network-address + + + network-address + + + 60 + seconds + true + + + + + + + String + + @text/channel-type.gree.mode.description + + + + + + + + + + + + + + + Number:Temperature + + @text/channel-type.gree.temperature.description + + + + Switch + + @text/channel-type.gree.air.description + + + Switch + + @text/channel-type.gree.dry.description + + + Switch + + @text/channel-type.gree.turbo.description + + + Number + + @text/channel-type.gree.windspeed.description + + + + + + + + + + + + + String + + @text/channel-type.gree.quiet.description + + + + + + + + + + Number + + @text/channel-type.gree.swingupdown.description + + + + + + + + + + + + + + Number + + @text/channel-type.gree.swingleftright.description + + + + + + + + + + + + + + Switch + + @text/channel-type.gree.powersave.description + + + Switch + + @text/channel-type.gree.light.description + + + Switch + + @text/channel-type.gree.health.description + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 69f530359ab66..7d2c1aa0e4cad 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -106,6 +106,7 @@ org.openhab.binding.goecharger org.openhab.binding.globalcache org.openhab.binding.gpstracker + org.openhab.binding.gree org.openhab.binding.groheondus org.openhab.binding.harmonyhub org.openhab.binding.hdanywhere From 86fab7d4b619b2802e3df3c8509baae3626d5a8a Mon Sep 17 00:00:00 2001 From: lolodomo Date: Wed, 8 Jul 2020 14:51:00 +0200 Subject: [PATCH 42/85] [hue] Reduce log level in refresh jobs (#8088) Signed-off-by: Laurent Garnier --- .../hue/internal/handler/HueBridgeHandler.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java index 6ac9674ccb63f..750170c594b95 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java @@ -179,7 +179,7 @@ protected void doConnectedRun() throws IOException, ApiException { final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId); if (sensorStatusListener == null) { - logger.debug("Hue sensor '{}' added.", sensorId); + logger.trace("Hue sensor '{}' added.", sensorId); if (discovery != null && !lastSensorStateCopy.containsKey(sensorId)) { discovery.addSensorDiscovery(sensor); @@ -196,7 +196,7 @@ protected void doConnectedRun() throws IOException, ApiException { // Check for removed sensors lastSensorStateCopy.forEach((sensorId, sensor) -> { - logger.debug("Hue sensor '{}' removed.", sensorId); + logger.trace("Hue sensor '{}' removed.", sensorId); lastSensorStates.remove(sensorId); final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId); @@ -230,7 +230,7 @@ protected void doConnectedRun() throws IOException, ApiException { final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId); if (lightStatusListener == null) { - logger.debug("Hue light '{}' added.", lightId); + logger.trace("Hue light '{}' added.", lightId); if (discovery != null && !lastLightStateCopy.containsKey(lightId)) { discovery.addLightDiscovery(fullLight); @@ -247,7 +247,7 @@ protected void doConnectedRun() throws IOException, ApiException { // Check for removed lights lastLightStateCopy.forEach((lightId, light) -> { - logger.debug("Hue light '{}' removed.", lightId); + logger.trace("Hue light '{}' removed.", lightId); lastLightStates.remove(lightId); final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId); @@ -313,7 +313,7 @@ protected void doConnectedRun() throws IOException, ApiException { final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId); if (groupStatusListener == null) { - logger.debug("Hue group '{}' ({}) added (nb lights {}).", groupId, fullGroup.getName(), + logger.trace("Hue group '{}' ({}) added (nb lights {}).", groupId, fullGroup.getName(), fullGroup.getLights().size()); if (discovery != null && !lastGroupStateCopy.containsKey(groupId)) { @@ -331,7 +331,7 @@ protected void doConnectedRun() throws IOException, ApiException { // Check for removed groups lastGroupStateCopy.forEach((groupId, group) -> { - logger.debug("Hue group '{}' removed.", groupId); + logger.trace("Hue group '{}' removed.", groupId); lastGroupStates.remove(groupId); final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId); From b315a5e037719bd43e178d5dbcea25ff299fd967 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Wed, 8 Jul 2020 17:44:23 +0200 Subject: [PATCH 43/85] [innogysmarthome] Fix possible resource leak (#8080) Related to #8027 Signed-off-by: Laurent Garnier --- .../innogysmarthome/internal/InnogyWebSocket.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/InnogyWebSocket.java b/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/InnogyWebSocket.java index 09a87134adb43..93e0195a2578d 100644 --- a/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/InnogyWebSocket.java +++ b/bundles/org.openhab.binding.innogysmarthome/src/main/java/org/openhab/binding/innogysmarthome/internal/InnogyWebSocket.java @@ -97,6 +97,15 @@ public synchronized void stop() { session = null; logger.trace("Stopping websocket ignored - was not running."); } + if (client != null) { + try { + client.stop(); + client.destroy(); + } catch (Exception e) { + logger.debug("Stopping websocket failed", e); + } + client = null; + } } /** From ba61eda76b8b6e558bfada87af9c5c230e931f92 Mon Sep 17 00:00:00 2001 From: Mark Hilbush Date: Wed, 8 Jul 2020 14:03:47 -0400 Subject: [PATCH 44/85] [squeezebox] Implement like/unlike for remote streaming services (#7396) * Implement like/unlike for remote streaming services * Fix formatting * Address review feedback * Combine like/unlike to single rate channel * Address review comments Signed-off-by: Mark Hilbush --- .../org.openhab.binding.squeezebox/README.md | 21 +++- .../internal/SqueezeBoxBindingConstants.java | 1 + .../internal/SqueezeBoxHandlerFactory.java | 44 +++---- ...ezeBoxStateDescriptionOptionsProvider.java | 12 +- .../SqueezeBoxPlayerDiscoveryParticipant.java | 5 + .../squeezebox/internal/dto/ButtonDTO.java | 65 +++++++++++ .../internal/dto/ButtonDTODeserializer.java | 54 +++++++++ .../squeezebox/internal/dto/ButtonsDTO.java | 53 +++++++++ .../internal/dto/RemoteMetaDTO.java | 77 +++++++++++++ .../internal/dto/StatusResponseDTO.java | 47 ++++++++ .../internal/dto/StatusResultDTO.java | 93 +++++++++++++++ .../SqueezeBoxNotificationListener.java | 4 + .../SqueezeBoxPlayerEventListener.java | 2 + .../handler/SqueezeBoxPlayerHandler.java | 49 +++++--- .../handler/SqueezeBoxServerHandler.java | 108 ++++++++++++++++-- .../resources/ESH-INF/thing/thing-types.xml | 6 + 16 files changed, 574 insertions(+), 67 deletions(-) create mode 100644 bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTO.java create mode 100644 bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTODeserializer.java create mode 100644 bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonsDTO.java create mode 100644 bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/RemoteMetaDTO.java create mode 100644 bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResponseDTO.java create mode 100644 bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResultDTO.java diff --git a/bundles/org.openhab.binding.squeezebox/README.md b/bundles/org.openhab.binding.squeezebox/README.md index 41de13e1d93c4..0a5ee9d39eabe 100644 --- a/bundles/org.openhab.binding.squeezebox/README.md +++ b/bundles/org.openhab.binding.squeezebox/README.md @@ -114,6 +114,7 @@ All devices support some of the following channels: | ircode | String | Received IR code | | numberPlaylistTracks | Number | Number of playlist tracks | | playFavorite | String | ID of Favorite to play (channel's state options contains available favorites) | +| rate | Switch | "Like" or "unlike" the currently playing song (if supported by the streaming service) | ## Example .Items File @@ -207,14 +208,28 @@ then end ``` +## Rating Songs + +Some streaming services, such as Pandora and Slacker, all songs to be rated. +When playing from these streaming services, sending commands to the `rate` channel can be used to *like* or *unlike* the currently playing song. +Sending the ON command will *like* the song. +Sending the OFF command will *unlike* the song. +If the streaming service doesn't support rating, sending commands to the `rate` channel has no effect. + ### Known Issues -- There are some versions of squeezelite that will not correctly play very short duration mp3 files. Versions of squeezelite after v1.7 and before v1.8.6 will not play very short duration mp3 files reliably. For example, if you're using piCorePlayer (which uses squeezelite), please check your version of squeezelite if you're having trouble playing notifications. This bug has been fixed in squeezelite version 1.8.6-985, which is included in piCorePlayer version 3.20. +- There are some versions of squeezelite that will not correctly play very short duration mp3 files. +Versions of squeezelite after v1.7 and before v1.8.6 will not play very short duration mp3 files reliably. +For example, if you're using piCorePlayer (which uses squeezelite), please check your version of squeezelite if you're having trouble playing notifications. +This bug has been fixed in squeezelite version 1.8.6-985, which is included in piCorePlayer version 3.20. - When streaming from a remote service (such as Pandora or Spotify), after the notification plays, the Squeezebox Server starts playing a new track, instead of picking up from where it left off on the currently playing track. -- There have been reports that notifications do not play reliably, or do not play at all, when using Logitech Media Server (LMS) version 7.7.5. Therefore, it is recommended that the LMS be on a more current version than 7.7.5. +- There have been reports that notifications do not play reliably, or do not play at all, when using Logitech Media Server (LMS) version 7.7.5. +Therefore, it is recommended that the LMS be on a more current version than 7.7.5. - There have been reports that the LMS does not play some WAV files reliably. If you're using a TTS service that produces WAV files, and the notifications are not playing, try using an MP3-formatted TTS notification. +This issue reportedly was [fixed in the LMS](https://github.com/Logitech/slimserver/issues/307) by accepting additional MIME types for WAV files. -- The LMS treats player MAC addresses as case-sensitive. Therefore, the case of MAC addresses in the Squeeze Player thing configuration must match the case displayed on the *Information* tab in the LMS Settings. +- The LMS treats player MAC addresses as case-sensitive. +Therefore, the case of MAC addresses in the Squeeze Player thing configuration must match the case displayed on the *Information* tab in the LMS Settings. diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxBindingConstants.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxBindingConstants.java index 657307dc35ce0..ded4d188980b5 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxBindingConstants.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxBindingConstants.java @@ -67,4 +67,5 @@ public class SqueezeBoxBindingConstants { public static final String CHANNEL_NAME = "name"; public static final String CHANNEL_MODEL = "model"; public static final String CHANNEL_FAVORITES_PLAY = "playFavorite"; + public static final String CHANNEL_RATE = "rate"; } diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxHandlerFactory.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxHandlerFactory.java index 82460f10c9df3..5867daf39ecec 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxHandlerFactory.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxHandlerFactory.java @@ -41,6 +41,7 @@ import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxServerHandler; import org.osgi.framework.ServiceRegistration; import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; @@ -65,16 +66,24 @@ public class SqueezeBoxHandlerFactory extends BaseThingHandlerFactory { private Map> discoveryServiceRegs = new HashMap<>(); - private AudioHTTPServer audioHTTPServer; - private NetworkAddressService networkAddressService; + private final AudioHTTPServer audioHTTPServer; + private final NetworkAddressService networkAddressService; + private final SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider; private Map> audioSinkRegistrations = new ConcurrentHashMap<>(); - private SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider; - // Callback url (scheme+server+port) to use for playing notification sounds private String callbackUrl = null; + @Activate + public SqueezeBoxHandlerFactory(@Reference AudioHTTPServer audioHTTPServer, + @Reference NetworkAddressService networkAddressService, + @Reference SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider) { + this.audioHTTPServer = audioHTTPServer; + this.networkAddressService = networkAddressService; + this.stateDescriptionProvider = stateDescriptionProvider; + } + @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); @@ -197,31 +206,4 @@ private String createCallbackUrl() { return "http://" + ipAddress + ":" + port; } - - @Reference - protected void setAudioHTTPServer(AudioHTTPServer audioHTTPServer) { - this.audioHTTPServer = audioHTTPServer; - } - - protected void unsetAudioHTTPServer(AudioHTTPServer audioHTTPServer) { - this.audioHTTPServer = null; - } - - @Reference - protected void setNetworkAddressService(NetworkAddressService networkAddressService) { - this.networkAddressService = networkAddressService; - } - - protected void unsetNetworkAddressService(NetworkAddressService networkAddressService) { - this.networkAddressService = null; - } - - @Reference - protected void setDynamicStateDescriptionProvider(SqueezeBoxStateDescriptionOptionsProvider provider) { - this.stateDescriptionProvider = provider; - } - - protected void unsetDynamicStateDescriptionProvider(SqueezeBoxStateDescriptionOptionsProvider provider) { - this.stateDescriptionProvider = null; - } } diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxStateDescriptionOptionsProvider.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxStateDescriptionOptionsProvider.java index ec8b60a4be0ba..9b7fe06344c0d 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxStateDescriptionOptionsProvider.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/SqueezeBoxStateDescriptionOptionsProvider.java @@ -16,6 +16,7 @@ import org.eclipse.smarthome.core.thing.binding.BaseDynamicStateDescriptionProvider; import org.eclipse.smarthome.core.thing.i18n.ChannelTypeI18nLocalizationService; import org.eclipse.smarthome.core.thing.type.DynamicStateDescriptionProvider; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -29,14 +30,9 @@ @NonNullByDefault public class SqueezeBoxStateDescriptionOptionsProvider extends BaseDynamicStateDescriptionProvider { - @Reference - protected void setChannelTypeI18nLocalizationService( - final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + @Activate + public SqueezeBoxStateDescriptionOptionsProvider( + @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService; } - - protected void unsetChannelTypeI18nLocalizationService( - final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { - this.channelTypeI18nLocalizationService = null; - } } diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/discovery/SqueezeBoxPlayerDiscoveryParticipant.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/discovery/SqueezeBoxPlayerDiscoveryParticipant.java index a464b827944e0..d31c1ac746b54 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/discovery/SqueezeBoxPlayerDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/discovery/SqueezeBoxPlayerDiscoveryParticipant.java @@ -67,6 +67,7 @@ public SqueezeBoxPlayerDiscoveryParticipant(SqueezeBoxServerHandler squeezeBoxSe protected void startScan() { logger.debug("startScan invoked in SqueezeBoxPlayerDiscoveryParticipant"); this.squeezeBoxServerHandler.requestPlayers(); + this.squeezeBoxServerHandler.requestFavorites(); } /* @@ -206,4 +207,8 @@ public void updateFavoritesListEvent(List favorites) { @Override public void sourceChangeEvent(String mac, String source) { } + + @Override + public void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand) { + } } diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTO.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTO.java new file mode 100644 index 0000000000000..38681045c855b --- /dev/null +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTO.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.squeezebox.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link ButtonDTO} represents a custom button that overrides existing + * button functionality. For example, "like song" replaces the repeat button. + * + * @author Mark Hilbush - Initial contribution + */ +public class ButtonDTO { + + /** + * Indicates whether button is standard or custom + */ + public Boolean custom; + + /** + * Indicates if standard button is enabled or disabled + */ + public Boolean enabled; + + /** + * Concatenation of elements of command array + */ + public String command; + + /** + * Currently not used + */ + @SerializedName("icon") + public String icon; + + /** + * Currently not used + */ + @SerializedName("jiveStyle") + public String jiveStyle; + + /** + * Currently not used + */ + @SerializedName("tooltip") + public String toolTip; + + public boolean isCustom() { + return custom == null ? Boolean.FALSE : custom; + } + + public boolean isEnabled() { + return enabled == null ? Boolean.FALSE : enabled; + } +} diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTODeserializer.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTODeserializer.java new file mode 100644 index 0000000000000..0e235edb856d3 --- /dev/null +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonDTODeserializer.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.squeezebox.internal.dto; + +import java.lang.reflect.Type; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * The {@link ButtonDTODeserializer} is responsible for deserializing a button object, which + * can either be an Integer, or a custom button specification. + * + * @author Mark Hilbush - Initial contribution + */ +public class ButtonDTODeserializer implements JsonDeserializer { + + @Override + public ButtonDTO deserialize(JsonElement jsonElement, Type tyoeOfT, JsonDeserializationContext context) + throws JsonParseException { + ButtonDTO button = null; + if (jsonElement.isJsonPrimitive() && jsonElement.getAsJsonPrimitive().isNumber()) { + Integer value = jsonElement.getAsInt(); + button = new ButtonDTO(); + button.custom = false; + button.enabled = value != 0; + } else if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + button = new ButtonDTO(); + button.custom = true; + button.icon = jsonObject.get("icon").getAsString(); + button.jiveStyle = jsonObject.get("jiveStyle").getAsString(); + button.toolTip = jsonObject.get("tooltip").getAsString(); + button.command = StreamSupport.stream(jsonObject.getAsJsonArray("command").spliterator(), false) + .map(JsonElement::getAsString).collect(Collectors.joining(" ")); + } + return button; + } +} diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonsDTO.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonsDTO.java new file mode 100644 index 0000000000000..f65c028fa695d --- /dev/null +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/ButtonsDTO.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.squeezebox.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link ButtonsDTO} contains information about the forward, rewind, repeat, + * and shuffle buttons, including any custom definitions, such as replacing repeat + * and shuffle with like and unlike, respectively. + * + * @author Mark Hilbush - Initial contribution + */ +public class ButtonsDTO { + + /** + * Indicates if forward button is enabled/disabled, + * or if there is a custom button definition. + */ + @SerializedName("fwd") + public ButtonDTO forward; + + /** + * Indicates if rewind button is enabled/disabled, + * or if there is a custom button definition. + */ + @SerializedName("rew") + public ButtonDTO rewind; + + /** + * Indicates if repeat button is enabled/disabled, + * or if there is a custom button definition. + */ + @SerializedName("repeat") + public ButtonDTO repeat; + + /** + * Indicates if shuffle button is enabled/disabled, + * or if there is a custom button definition. + */ + @SerializedName("shuffle") + public ButtonDTO shuffle; +} diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/RemoteMetaDTO.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/RemoteMetaDTO.java new file mode 100644 index 0000000000000..196da72b20d60 --- /dev/null +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/RemoteMetaDTO.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.squeezebox.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link RemoteMetaDTO} contains remote metadata information, including button and + * button override functionality. + * + * @author Mark Hilbush - Initial contribution + */ +public class RemoteMetaDTO { + + /** + * Contains button specifications for forward, rewind, repeat, shuffle + */ + public ButtonsDTO buttons; + + /** + * Currently unused + */ + @SerializedName("id") + public String id; + + /** + * Currently unused + */ + @SerializedName("title") + public String title; + + /** + * Currently unused + */ + @SerializedName("artist") + public String artist; + + /** + * Currently unused + */ + @SerializedName("album") + public String album; + + /** + * Currently unused + */ + @SerializedName("artwork_url") + public String artworkUrl; + + /** + * Currently unused + */ + @SerializedName("coverart") + public String coverart; + + /** + * Currently unused + */ + @SerializedName("coverid") + public String coverid; + + /** + * Currently unused + */ + @SerializedName("year") + public String year; +} diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResponseDTO.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResponseDTO.java new file mode 100644 index 0000000000000..c463f15476e3c --- /dev/null +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResponseDTO.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.squeezebox.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link StatusResponseDTO} is the response received from a player status request. + * + * @author Mark Hilbush - Initial contribution + */ +public class StatusResponseDTO { + + /** + * Id. Currently unused. + */ + @SerializedName("id") + public String id; + + /** + * Method name. Normally "slim.request" + */ + @SerializedName("method") + public String method; + + /** + * Parameters passed in the query. Currently unused. + */ + @SerializedName("params") + public Object params; + + /** + * Contains the result of the query + */ + @SerializedName("result") + public StatusResultDTO result; +} diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResultDTO.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResultDTO.java new file mode 100644 index 0000000000000..63a37607a6e54 --- /dev/null +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/dto/StatusResultDTO.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.squeezebox.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link StatusResultDTO} represents the result of a status request. + * + * @author Mark Hilbush - Initial contribution + */ +public class StatusResultDTO { + + /** + * Remote metadata information, including button definitions/redefinitions. + */ + @SerializedName("remoteMeta") + public RemoteMetaDTO remoteMeta; + + /** + * These remaining fields are currently unused by the binding, + * as they also are returned by the Command Line Interface (CLI). + */ + @SerializedName("current_title") + public String currentTitle; + + @SerializedName("digital_volume_control") + public Integer digitalVolumeControl; + + @SerializedName("duration") + public Double duration; + + @SerializedName("mixer volume") + public Integer mixerVolume; + + @SerializedName("player_connected") + public Integer playerConnected; + + @SerializedName("player_ip") + public String playerIpAddress; + + @SerializedName("player_name") + public String playerName; + + @SerializedName("playlist mode") + public String playlistMode; + + @SerializedName("playlist repeat") + public Integer playlistRepeat; + + @SerializedName("playlist shuffle") + public Integer playlistShuffle; + + @SerializedName("playlist_cur_index") + public String playListCurrentIndex; + + @SerializedName("playlist_timestamp") + public String playlistTimestamp; + + @SerializedName("playlist_tracks") + public Integer playlistTracks; + + @SerializedName("power") + public String power; + + @SerializedName("rate") + public String rate; + + @SerializedName("remote") + public String remote; + + @SerializedName("repeating_stream") + public Integer repeatingStream; + + @SerializedName("seq_no") + public Integer sequenceNumber; + + @SerializedName("signalstrength") + public Integer signalStrength; + + @SerializedName("time") + public String time; +} diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxNotificationListener.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxNotificationListener.java index 60bb6318659a4..1318668b71a20 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxNotificationListener.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxNotificationListener.java @@ -216,4 +216,8 @@ public void updateFavoritesListEvent(List favorites) { @Override public void sourceChangeEvent(String mac, String source) { } + + @Override + public void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand) { + } } diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerEventListener.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerEventListener.java index 913bff31d4aad..8fca220d1e272 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerEventListener.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerEventListener.java @@ -80,4 +80,6 @@ public interface SqueezeBoxPlayerEventListener { void updateFavoritesListEvent(List favorites); void sourceChangeEvent(String mac, String source); + + void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand); } diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerHandler.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerHandler.java index f533af50f0f94..c2d43f36e306d 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerHandler.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxPlayerHandler.java @@ -68,6 +68,7 @@ * @author Patrik Gfeller - Timeout for TTS messages increased from 30 to 90s. * @author Mark Hilbush - Get favorites from server and play favorite * @author Mark Hilbush - Convert sound notification volume from channel to config parameter + * @author Mark Hilbush - Add like/unlike functionality */ public class SqueezeBoxPlayerHandler extends BaseThingHandler implements SqueezeBoxPlayerEventListener { private final Logger logger = LoggerFactory.getLogger(SqueezeBoxPlayerHandler.class); @@ -84,7 +85,7 @@ public class SqueezeBoxPlayerHandler extends BaseThingHandler implements Squeeze /** * Keeps current track time */ - ScheduledFuture timeCounterJob; + private ScheduledFuture timeCounterJob; /** * Local reference to our bridge @@ -120,6 +121,9 @@ public class SqueezeBoxPlayerHandler extends BaseThingHandler implements Squeeze private static final ExpiringCacheMap IMAGE_CACHE = new ExpiringCacheMap<>( TimeUnit.MINUTES.toMillis(15)); // 15min + private String likeCommand; + private String unlikeCommand; + /** * Creates SqueezeBox Player Handler * @@ -138,7 +142,7 @@ public void initialize() { mac = getConfig().as(SqueezeBoxPlayerConfig.class).mac; timeCounter(); updateBridgeStatus(); - logger.debug("player thing {} initialized.", getThing().getUID()); + logger.debug("player thing {} initialized with mac {}", getThing().getUID(), mac); } @Override @@ -147,12 +151,17 @@ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { } private void updateBridgeStatus() { - ThingStatus bridgeStatus = getBridge().getStatus(); - if (bridgeStatus == ThingStatus.ONLINE && getThing().getStatus() != ThingStatus.ONLINE) { - updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE); - squeezeBoxServerHandler = (SqueezeBoxServerHandler) getBridge().getHandler(); - } else if (bridgeStatus == ThingStatus.OFFLINE) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + Thing bridge = getBridge(); + if (bridge != null) { + squeezeBoxServerHandler = (SqueezeBoxServerHandler) bridge.getHandler(); + ThingStatus bridgeStatus = bridge.getStatus(); + if (bridgeStatus == ThingStatus.ONLINE && getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE); + } else if (bridgeStatus == ThingStatus.OFFLINE) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge not found"); } } @@ -167,18 +176,16 @@ public void dispose() { if (squeezeBoxServerHandler != null) { squeezeBoxServerHandler.removePlayerCache(mac); } - logger.debug("player thing {} disposed.", getThing().getUID()); + logger.debug("player thing {} disposed for mac {}", getThing().getUID(), mac); super.dispose(); } @Override public void handleCommand(ChannelUID channelUID, Command command) { if (squeezeBoxServerHandler == null) { - logger.info("player thing {} has no server configured, ignoring command: {}", getThing().getUID(), command); + logger.debug("Player {} has no server configured, ignoring command: {}", getThing().getUID(), command); return; } - String mac = getConfigAs(SqueezeBoxPlayerConfig.class).mac; - // Some of the code below is not designed to handle REFRESH, only reply to channels where cached values exist if (command == RefreshType.REFRESH) { String channelID = channelUID.getId(); @@ -293,6 +300,13 @@ public void handleCommand(ChannelUID channelUID, Command command) { case CHANNEL_FAVORITES_PLAY: squeezeBoxServerHandler.playFavorite(mac, command.toString()); break; + case CHANNEL_RATE: + if (command.equals(OnOffType.ON)) { + squeezeBoxServerHandler.rate(mac, likeCommand); + } else if (command.equals(OnOffType.OFF)) { + squeezeBoxServerHandler.rate(mac, unlikeCommand); + } + break; default: break; } @@ -493,9 +507,18 @@ public void irCodeChangeEvent(String mac, String ircode) { } } + @Override + public void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand) { + if (isMe(mac)) { + this.likeCommand = likeCommand; + this.unlikeCommand = unlikeCommand; + logger.trace("Player {} got a button change event: like='{}' unlike='{}'", mac, likeCommand, unlikeCommand); + } + } + @Override public void updateFavoritesListEvent(List favorites) { - logger.debug("Player {} updating favorites list", mac); + logger.trace("Player {} updating favorites list with {} favorites", mac, favorites.size()); List options = new ArrayList<>(); for (Favorite favorite : favorites) { options.add(new StateOption(favorite.shortId, favorite.name)); diff --git a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxServerHandler.java b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxServerHandler.java index acb37c1d99c23..9a8080435f345 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxServerHandler.java +++ b/bundles/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/internal/handler/SqueezeBoxServerHandler.java @@ -24,14 +24,17 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Future; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.StringUtils; @@ -47,11 +50,20 @@ import org.eclipse.smarthome.core.thing.binding.ThingHandler; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.UnDefType; +import org.eclipse.smarthome.io.net.http.HttpRequestBuilder; import org.openhab.binding.squeezebox.internal.config.SqueezeBoxServerConfig; +import org.openhab.binding.squeezebox.internal.dto.ButtonDTO; +import org.openhab.binding.squeezebox.internal.dto.ButtonDTODeserializer; +import org.openhab.binding.squeezebox.internal.dto.ButtonsDTO; +import org.openhab.binding.squeezebox.internal.dto.StatusResponseDTO; import org.openhab.binding.squeezebox.internal.model.Favorite; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; + /** * Handles connection and event handling to a SqueezeBox Server. * @@ -66,6 +78,7 @@ * @author Philippe Siem - Improve refresh of cover art url,remote title, artist, album, genre, year. * @author Patrik Gfeller - Support for mixer volume message added * @author Mark Hilbush - Get favorites from LMS; update channel and send to players + * @author Mark Hilbush - Add like/unlike functionality */ public class SqueezeBoxServerHandler extends BaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(SqueezeBoxServerHandler.class); @@ -86,6 +99,8 @@ public class SqueezeBoxServerHandler extends BaseBridgeHandler { private static final String CHANNEL_CONFIG_QUOTE_LIST = "quoteList"; + private static final String JSONRPC_STATUS_REQUEST = "{\"id\":1,\"method\":\"slim.request\",\"params\":[\"@@MAC@@\",[\"status\",\"-\",\"tags:yagJlNKjcB\"]]}"; + private List squeezeBoxPlayerListeners = Collections .synchronizedList(new ArrayList<>()); @@ -106,6 +121,11 @@ public class SqueezeBoxServerHandler extends BaseBridgeHandler { private String password; + private final Gson gson = new GsonBuilder().registerTypeAdapter(ButtonDTO.class, new ButtonDTODeserializer()) + .create(); + private String jsonRpcUrl; + private String basicAuthorization; + public SqueezeBoxServerHandler(Bridge bridge) { super(bridge); } @@ -275,6 +295,12 @@ public void playFavorite(String mac, String favorite) { sendCommand(mac + " favorites playlist play item_id:" + favorite); } + public void rate(String mac, String rateCommand) { + if (rateCommand != null) { + sendCommand(mac + " " + rateCommand); + } + } + /** * Send a generic command to a given player * @@ -306,6 +332,9 @@ public void login() { if (StringUtils.isEmpty(userId)) { return; } + // Create basic auth string for jsonrpc interface + basicAuthorization = new String( + Base64.getEncoder().encode((userId + ":" + password).getBytes(StandardCharsets.UTF_8))); logger.debug("Logging into Squeeze Server using userId={}", userId); sendCommand("login " + userId + " " + password); } @@ -361,6 +390,9 @@ private void connect() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "host is not set"); return; } + // Create URL for jsonrpc interface + jsonRpcUrl = String.format("http://%s:%d/jsonrpc.js", host, webport); + try { clientSocket = new Socket(host, cliport); } catch (IOException e) { @@ -377,7 +409,6 @@ private void connect() { } catch (IllegalThreadStateException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } - // Mark the server ONLINE. bridgeStatusChanged will cause the players to come ONLINE updateStatus(ThingStatus.ONLINE); } @@ -420,13 +451,14 @@ public void terminate() { public void run() { BufferedReader reader = null; boolean endOfStream = false; + ScheduledFuture requestFavoritesJob = null; try { reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); login(); updateStatus(ThingStatus.ONLINE); requestPlayers(); - requestFavorites(); + requestFavoritesJob = scheduleRequestFavorites(); sendCommand("listen 1"); String message = null; @@ -474,7 +506,10 @@ public void run() { "end of stream on socket read"); scheduleReconnect(); } - + if (requestFavoritesJob != null && !requestFavoritesJob.isDone()) { + requestFavoritesJob.cancel(true); + logger.debug("Canceled request favorites job"); + } logger.debug("Squeeze Server listener exiting."); } @@ -598,7 +633,6 @@ private void handleMixerMessage(String mac, String[] messageParts) { switch (action) { case "volume": String volumeStringValue = decode(messageParts[3]); - updatePlayer(new PlayerUpdateEvent() { @Override public void updateListener(SqueezeBoxPlayerEventListener listener) { @@ -836,6 +870,8 @@ private void handlePlaylistMessage(final String mac, String[] messageParts) { String mode; if (action.equals("newsong")) { mode = "play"; + // Execute in separate thread to avoid delaying listener + scheduler.execute(() -> updateCustomButtons(mac)); // Set the track duration to 0 updatePlayer(new PlayerUpdateEvent() { @Override @@ -862,7 +898,6 @@ public void updateListener(SqueezeBoxPlayerEventListener listener) { } final String value = mode; updatePlayer(new PlayerUpdateEvent() { - @Override public void updateListener(SqueezeBoxPlayerEventListener listener) { listener.modeChangeEvent(mac, value); @@ -872,9 +907,7 @@ public void updateListener(SqueezeBoxPlayerEventListener listener) { private void handleSourceChangeMessage(String mac, String rawSource) { String source = URLDecoder.decode(rawSource); - updatePlayer(new PlayerUpdateEvent() { - @Override public void updateListener(SqueezeBoxPlayerEventListener listener) { listener.sourceChangeEvent(mac, source); @@ -886,12 +919,10 @@ private void handlePrefsetMessage(final String mac, String[] messageParts) { if (messageParts.length < 5) { return; } - // server prefsets if (messageParts[2].equals("server")) { String function = messageParts[3]; String value = messageParts[4]; - if (function.equals("power")) { final boolean power = value.equals("1"); updatePlayer(new PlayerUpdateEvent() { @@ -903,7 +934,6 @@ public void updateListener(SqueezeBoxPlayerEventListener listener) { } else if (function.equals("volume")) { final int volume = (int) Double.parseDouble(value); updatePlayer(new PlayerUpdateEvent() { - @Override public void updateListener(SqueezeBoxPlayerEventListener listener) { listener.absoluteVolumeChangeEvent(mac, volume); @@ -914,8 +944,6 @@ public void updateListener(SqueezeBoxPlayerEventListener listener) { } private void handleFavorites(String message) { - logger.trace("Handle favorites message: {}", message); - String[] messageParts = message.split("\\s"); if (messageParts.length == 2 && "changed".equals(messageParts[1])) { // LMS informing us that favorites have changed; request an update to the favorites list @@ -998,6 +1026,62 @@ private void updateChannelFavoritesList(List favorites) { updateState(CHANNEL_FAVORITES_LIST, new StringType(favoritesList)); } } + + private ScheduledFuture scheduleRequestFavorites() { + // Delay the execution to give the player thing handlers a chance to initialize + return scheduler.schedule(SqueezeBoxServerHandler.this::requestFavorites, 3L, TimeUnit.SECONDS); + } + + private void updateCustomButtons(final String mac) { + String response = executePost(jsonRpcUrl, JSONRPC_STATUS_REQUEST.replace("@@MAC@@", mac)); + if (response != null) { + logger.trace("Status response: {}", response); + String likeCommand = null; + String unlikeCommand = null; + try { + StatusResponseDTO status = gson.fromJson(response, StatusResponseDTO.class); + if (status != null && status.result != null && status.result.remoteMeta != null + && status.result.remoteMeta.buttons != null) { + ButtonsDTO buttons = status.result.remoteMeta.buttons; + if (buttons.repeat != null && buttons.repeat.isCustom()) { + likeCommand = buttons.repeat.command; + } + if (buttons.shuffle != null && buttons.shuffle.isCustom()) { + unlikeCommand = buttons.shuffle.command; + } + } + } catch (JsonSyntaxException e) { + logger.debug("JsonSyntaxException parsing status response: {}", response, e); + } + final String like = likeCommand; + final String unlike = unlikeCommand; + updatePlayer(new PlayerUpdateEvent() { + @Override + public void updateListener(SqueezeBoxPlayerEventListener listener) { + listener.buttonsChangeEvent(mac, like, unlike); + } + }); + } + } + + private String executePost(String url, String content) { + // @formatter:off + HttpRequestBuilder builder = HttpRequestBuilder.postTo(url) + .withTimeout(Duration.ofSeconds(5)) + .withContent(content) + .withHeader("charset", "utf-8") + .withHeader("Content-Type", "application/json"); + // @formatter:on + if (basicAuthorization != null) { + builder = builder.withHeader("Authorization", "Basic " + basicAuthorization); + } + try { + return builder.getContentAsString(); + } catch (IOException e) { + logger.debug("Bridge: IOException on jsonrpc call: {}", e.getMessage(), e); + return null; + } + } } /** diff --git a/bundles/org.openhab.binding.squeezebox/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.squeezebox/src/main/resources/ESH-INF/thing/thing-types.xml index be1bbbb465402..24816a8060e8f 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/resources/ESH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.squeezebox/src/main/resources/ESH-INF/thing/thing-types.xml @@ -83,6 +83,7 @@ + @@ -298,4 +299,9 @@ Number of playlist tracks + + Switch + + Likes or unlikes the current song (if the service supports it) + From 5785cad4f861a383129283205c3f8467e210f322 Mon Sep 17 00:00:00 2001 From: eugen Date: Wed, 8 Jul 2020 20:09:00 +0200 Subject: [PATCH 45/85] [homekit] some cleanups (#8041) * some cleanups Signed-off-by: Eugen Freiter --- bundles/org.openhab.io.homekit/README.md | 5 +- .../java/org/openhab/io/homekit/Homekit.java | 6 +- .../io/homekit/internal/Debouncer.java | 2 +- .../internal/HomekitAccessoryRegistry.java | 2 - .../internal/HomekitAccessoryUpdater.java | 10 +- .../homekit/internal/HomekitAuthInfoImpl.java | 35 +++-- .../internal/HomekitChangeListener.java | 34 ++--- .../internal/HomekitCommandExtension.java | 17 +-- .../homekit/internal/HomekitCommandType.java | 2 +- .../io/homekit/internal/HomekitException.java | 3 + .../io/homekit/internal/HomekitImpl.java | 36 ++--- .../homekit/internal/HomekitOHItemProxy.java | 6 +- .../homekit/internal/HomekitTaggedItem.java | 23 ++-- .../AbstractHomekitAccessoryImpl.java | 60 +++++--- .../accessories/BooleanItemReader.java | 20 ++- .../accessories/HomekitAccessoryFactory.java | 46 +++---- .../HomekitCarbonDioxideSensorImpl.java | 19 +-- .../HomekitCarbonMonoxideSensorImpl.java | 19 +-- .../HomekitCharacteristicFactory.java | 129 +++++++++--------- .../accessories/HomekitContactSensorImpl.java | 17 +-- .../internal/accessories/HomekitFanImpl.java | 26 ++-- .../HomekitGarageDoorOpenerImpl.java | 58 ++++---- .../accessories/HomekitHeaterCoolerImpl.java | 50 +++---- .../HomekitHumiditySensorImpl.java | 5 +- .../accessories/HomekitLeakSensorImpl.java | 18 +-- .../accessories/HomekitLightbulbImpl.java | 2 +- .../internal/accessories/HomekitLockImpl.java | 44 +++--- .../accessories/HomekitMotionSensorImpl.java | 8 +- .../HomekitOccupancySensorImpl.java | 17 +-- .../accessories/HomekitOutletImpl.java | 23 ++-- .../HomekitSecuritySystemImpl.java | 22 +-- .../accessories/HomekitSmokeSensorImpl.java | 16 +-- .../accessories/HomekitSpeakerImpl.java | 12 +- .../accessories/HomekitSwitchImpl.java | 23 ++-- .../HomekitTemperatureSensorImpl.java | 6 +- .../accessories/HomekitThermostatImpl.java | 8 +- .../accessories/HomekitValveImpl.java | 35 ++--- .../HomekitWindowCoveringImpl.java | 25 ++-- .../IncompleteAccessoryException.java | 2 + 39 files changed, 412 insertions(+), 479 deletions(-) diff --git a/bundles/org.openhab.io.homekit/README.md b/bundles/org.openhab.io.homekit/README.md index 552cd6adf0507..8e111423ecdec 100644 --- a/bundles/org.openhab.io.homekit/README.md +++ b/bundles/org.openhab.io.homekit/README.md @@ -439,10 +439,9 @@ The HomeKit valve accessory supports following 2 optional characteristics: - duration: this describes how long the valve should set "InUse" once it is activated. The duration changes will apply to the next operation. If valve is already active then duration changes have no effect. -- remaining duration: this describes the remaining duration on the valve. Notifications on this characteristic must only - be used if the remaining duration increases/decreases from the accessoryʼs usual countdown of remaining duration. +- remaining duration: this describes the remaining duration on the valve. Notifications on this characteristic must only be used if the remaining duration increases/decreases from the accessoryʼs usual countdown of remaining duration. -Upon valve activation in home app, home app starts to count down from the "duration" to "0" without contacting the server. Home app also does not trigger any acion if it remaining duration get 0. +Upon valve activation in home app, home app starts to count down from the "duration" to "0" without contacting the server. Home app also does not trigger any action if it remaining duration get 0. It is up to valve to have an own timer and stop valve once the timer is over. Some valves have such timer, e.g. pretty common for sprinklers. In case the valve has no timer capability, OpenHAB can take care on this - start an internal timer and send "Off" command to the valve once the timer is over. diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/Homekit.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/Homekit.java index 825775ce9edb7..5f87a4a4a0348 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/Homekit.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/Homekit.java @@ -31,9 +31,9 @@ public interface Homekit { * Refreshes the saved authentication info from the underlying storage service. If you * make changes to the saved authentication info, call this. * - * @throws IOException + * @throws IOException exception in case new auth info could not be published via mDNS */ - public void refreshAuthInfo() throws IOException; + void refreshAuthInfo() throws IOException; /** * HomeKit requests normally require authentication via the pairing mechanism. Use this @@ -52,5 +52,5 @@ public interface Homekit { /** * clear all pairings with HomeKit clients */ - public void clearHomekitPairings(); + void clearHomekitPairings(); } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/Debouncer.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/Debouncer.java index 380d539cf6811..d3d99a384a69c 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/Debouncer.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/Debouncer.java @@ -55,7 +55,7 @@ class Debouncer { * @param scheduler The scheduler implementation to use * @param delay The time after which to invoke action; each time [[Debouncer.call]] is invoked, this delay is * reset - * @param Clock The source from which we get the current time. This input should use the same source. Specified + * @param clock The source from which we get the current time. This input should use the same source. Specified * for testing purposes * @param action The action to invoke */ diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryRegistry.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryRegistry.java index e242a63bcd3ad..cce21e7c57bd5 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryRegistry.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryRegistry.java @@ -30,7 +30,6 @@ * @author Andy Lintner - Initial contribution */ class HomekitAccessoryRegistry { - private @Nullable HomekitRoot bridge; private final Map createdAccessories = new HashMap<>(); private int configurationRevision = 1; @@ -98,7 +97,6 @@ public synchronized void addRootAccessory(String itemName, HomekitAccessory acce if (bridge != null) { bridge.addAccessory(accessory); } - logger.trace("Added accessory {}", accessory.getId()); } public Map getAllAccessories() { diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryUpdater.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryUpdater.java index b3a259f04a295..40cf89dd976b5 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryUpdater.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryUpdater.java @@ -15,6 +15,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.items.Item; import org.eclipse.smarthome.core.items.StateChangeListener; @@ -33,7 +34,7 @@ * @author Andy Lintner - Initial contribution */ public class HomekitAccessoryUpdater { - private Logger logger = LoggerFactory.getLogger(HomekitAccessoryUpdater.class); + private final Logger logger = LoggerFactory.getLogger(HomekitAccessoryUpdater.class); private final ConcurrentMap subscriptionsByName = new ConcurrentHashMap<>(); public void subscribe(GenericItem item, HomekitCharacteristicChangeCallback callback) { @@ -78,7 +79,8 @@ public void unsubscribe(GenericItem item, String key) { } @FunctionalInterface - private static interface Subscription extends StateChangeListener { + @NonNullByDefault + private interface Subscription extends StateChangeListener { @Override void stateChanged(Item item, State oldState, State newState); @@ -90,8 +92,8 @@ default void stateUpdated(Item item, State state) { } private static class ItemKey { - public GenericItem item; - public String key; + public final GenericItem item; + public final String key; public ItemKey(GenericItem item, String key) { this.item = item; diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java index b387f7113dee5..ce63d789d27f5 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java @@ -34,6 +34,11 @@ */ public class HomekitAuthInfoImpl implements HomekitAuthInfo { private final Logger logger = LoggerFactory.getLogger(HomekitAuthInfoImpl.class); + public static final String STORAGE_KEY = "homekit"; + private static final String STORAGE_MAC = "mac"; + private static final String STORAGE_SALT = "salt"; + private static final String STORAGE_PRIVATE_KEY = "privateKey"; + private static final String STORAGE_USER_PREFIX = "user_"; private final Storage storage; private String mac; @@ -41,7 +46,7 @@ public class HomekitAuthInfoImpl implements HomekitAuthInfo { private byte[] privateKey; private final String pin; - public HomekitAuthInfoImpl(final Storage storage, final String pin) throws InvalidAlgorithmParameterException { + public HomekitAuthInfoImpl(Storage storage, String pin) throws InvalidAlgorithmParameterException { this.storage = storage; this.pin = pin; initializeStorage(); @@ -49,6 +54,7 @@ public HomekitAuthInfoImpl(final Storage storage, final String pin) throws Inval @Override public void createUser(String username, byte[] publicKey) { + logger.trace("Create user {}", username); storage.put(createUserKey(username), Base64.getEncoder().encodeToString(publicKey)); } @@ -84,16 +90,18 @@ public byte[] getUserPublicKey(String username) { @Override public void removeUser(String username) { + logger.trace("Remove user {}", username); storage.remove(createUserKey(username)); } @Override public boolean hasUser() { Collection keys = storage.getKeys(); - return keys.stream().filter(k -> isUserKey(k)).count() > 0; + return keys.stream().anyMatch(this::isUserKey); } public void clear() { + logger.trace("Clear all users"); for (String key : new HashSet<>(storage.getKeys())) { if (isUserKey(key)) { storage.remove(key); @@ -101,37 +109,34 @@ public void clear() { } } - private String createUserKey(final String username) { - return "user_" + username; + private String createUserKey(String username) { + return STORAGE_USER_PREFIX + username; } - private boolean isUserKey(final String key) { - return key.startsWith("user_"); + private boolean isUserKey(String key) { + return key.startsWith(STORAGE_USER_PREFIX); } private void initializeStorage() throws InvalidAlgorithmParameterException { - mac = storage.get("mac"); - @Nullable - Object saltConfig = storage.get("salt"); - @Nullable - Object privateKeyConfig = storage.get("privateKey"); - + mac = storage.get(STORAGE_MAC); + final @Nullable Object saltConfig = storage.get(STORAGE_SALT); + final @Nullable Object privateKeyConfig = storage.get(STORAGE_PRIVATE_KEY); if (mac == null) { logger.warn( "Could not find existing MAC in {}. Generating new MAC. This will require re-pairing of iOS devices.", storage.getClass().getName()); mac = HomekitServer.generateMac(); - storage.put("mac", mac); + storage.put(STORAGE_MAC, mac); } if (saltConfig == null) { salt = HomekitServer.generateSalt(); - storage.put("salt", salt.toString()); + storage.put(STORAGE_SALT, salt.toString()); } else { salt = new BigInteger(saltConfig.toString()); } if (privateKeyConfig == null) { privateKey = HomekitServer.generateKey(); - storage.put("privateKey", Base64.getEncoder().encodeToString(privateKey)); + storage.put(STORAGE_PRIVATE_KEY, Base64.getEncoder().encodeToString(privateKey)); } else { privateKey = Base64.getDecoder().decode(privateKeyConfig.toString()); } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitChangeListener.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitChangeListener.java index 39a944255e7cd..055e8777c5509 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitChangeListener.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitChangeListener.java @@ -16,7 +16,6 @@ import java.time.Duration; import java.util.Collection; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -24,6 +23,7 @@ import java.util.Set; import java.util.concurrent.ScheduledExecutorService; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.core.common.ThreadPoolManager; import org.eclipse.smarthome.core.items.GroupItem; import org.eclipse.smarthome.core.items.Item; @@ -46,6 +46,7 @@ * * @author Andy Lintner - Initial contribution */ +@NonNullByDefault public class HomekitChangeListener implements ItemRegistryChangeListener { private final Logger logger = LoggerFactory.getLogger(HomekitChangeListener.class); private final static String REVISION_CONFIG = "revision"; @@ -58,7 +59,7 @@ public class HomekitChangeListener implements ItemRegistryChangeListener { private HomekitSettings settings; private int lastAccessoryCount; - private Set pendingUpdates = new HashSet<>(); + private final Set pendingUpdates = new HashSet<>(); private final ScheduledExecutorService scheduler = ThreadPoolManager .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON); @@ -74,16 +75,16 @@ public class HomekitChangeListener implements ItemRegistryChangeListener { private final Debouncer applyUpdatesDebouncer; HomekitChangeListener(ItemRegistry itemRegistry, HomekitSettings settings, MetadataRegistry metadataRegistry, - final StorageService storageService) { + StorageService storageService) { this.itemRegistry = itemRegistry; this.settings = settings; this.metadataRegistry = metadataRegistry; - storage = storageService.getStorage("homekit"); + storage = storageService.getStorage(HomekitAuthInfoImpl.STORAGE_KEY); this.applyUpdatesDebouncer = new Debouncer("update-homekit-devices", scheduler, Duration.ofMillis(1000), Clock.systemUTC(), this::applyUpdates); itemRegistry.addRegistryChangeListener(this); - itemRegistry.getItems().stream().forEach(this::createRootAccessories); + itemRegistry.getItems().forEach(this::createRootAccessories); initialiseRevision(); logger.info("Created {} HomeKit items.", accessoryRegistry.getAllAccessories().size()); } @@ -91,14 +92,14 @@ public class HomekitChangeListener implements ItemRegistryChangeListener { private void initialiseRevision() { int revision; try { - revision = Integer.valueOf(storage.get(REVISION_CONFIG)); - } catch (java.lang.NumberFormatException e) { + revision = Integer.parseInt(storage.get(REVISION_CONFIG)); + } catch (NumberFormatException e) { revision = 1; storage.put(REVISION_CONFIG, "" + revision); } try { - lastAccessoryCount = Integer.valueOf(storage.get(ACCESSORY_COUNT)); - } catch (java.lang.NumberFormatException e) { + lastAccessoryCount = Integer.parseInt(storage.get(ACCESSORY_COUNT)); + } catch (NumberFormatException e) { lastAccessoryCount = 0; storage.put(ACCESSORY_COUNT, "" + accessoryRegistry.getAllAccessories().size()); } @@ -122,7 +123,7 @@ public void allItemsChanged(Collection oldItemNames) { * @param item The item that has been changed or removed. */ private synchronized void markDirty(Item item) { - logger.trace("Mark dirty item {}", item.getLabel()); + logger.trace("Mark dirty item {}", item.getName()); pendingUpdates.add(item.getName()); /* * If findMyAccessoryGroups fails because the accessory group has already been deleted, then we can count on a @@ -131,7 +132,6 @@ private synchronized void markDirty(Item item) { for (Item accessoryGroup : HomekitAccessoryFactory.getAccessoryGroups(item, itemRegistry, metadataRegistry)) { pendingUpdates.add(accessoryGroup.getName()); } - applyUpdatesDebouncer.call(); } @@ -159,10 +159,7 @@ public void makeNewConfigurationRevision() { private synchronized void applyUpdates() { logger.trace("apply updates"); - Iterator iter = pendingUpdates.iterator(); - - while (iter.hasNext()) { - String name = iter.next(); + for (final String name : pendingUpdates) { accessoryRegistry.remove(name); logger.trace(" add items {}", name); getItemOptional(name).ifPresent(this::createRootAccessories); @@ -220,26 +217,23 @@ public int getConfigurationRevision() { * creates one or more HomeKit items for given openhab item. * one openhab item can linked to several HomeKit accessories or characteristics. * - * @param item + * @param item openhab item */ private void createRootAccessories(Item item) { - logger.trace("create root accessory {}", item.getLabel()); final List> accessoryTypes = HomekitAccessoryFactory .getAccessoryTypes(item, metadataRegistry); final List groups = HomekitAccessoryFactory.getAccessoryGroups(item, itemRegistry, metadataRegistry); if (!accessoryTypes.isEmpty() && groups.stream().noneMatch(g -> g.getBaseItem() != null)) { // it has homekit accessory type and is not part of bigger homekit group item without baseItem, i.e. not // Group:Switch - logger.trace("Item {} is a HomeKit accessory of types {}", item.getName(), accessoryTypes); final HomekitOHItemProxy itemProxy = new HomekitOHItemProxy(item); - accessoryTypes.stream().forEach(rootAccessory -> createRootAccessory(new HomekitTaggedItem(itemProxy, + accessoryTypes.forEach(rootAccessory -> createRootAccessory(new HomekitTaggedItem(itemProxy, rootAccessory.getKey(), HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry)))); } } private void createRootAccessory(HomekitTaggedItem taggedItem) { try { - logger.trace("Adding HomeKit device {}", taggedItem.getItem().getUID()); accessoryRegistry.addRootAccessory(taggedItem.getName(), HomekitAccessoryFactory.create(taggedItem, metadataRegistry, updater, settings)); } catch (HomekitException e) { diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandExtension.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandExtension.java index aca612686324e..4be5f1701771c 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandExtension.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandExtension.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.concurrent.ExecutionException; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.io.console.Console; import org.eclipse.smarthome.io.console.extensions.AbstractConsoleCommandExtension; import org.eclipse.smarthome.io.console.extensions.ConsoleCommandExtension; @@ -31,6 +32,7 @@ * @author Andy Lintner - Initial contribution */ @Component(service = ConsoleCommandExtension.class) +@NonNullByDefault public class HomekitCommandExtension extends AbstractConsoleCommandExtension { private static final String SUBCMD_CLEAR_PAIRINGS = "clearPairings"; private static final String SUBCMD_LIST_ACCESSORIES = "list"; @@ -42,7 +44,8 @@ public class HomekitCommandExtension extends AbstractConsoleCommandExtension { private static final String LEGACY_SUBCMD_PRINT_ACCESSORY = "printAccessory"; private final Logger logger = LoggerFactory.getLogger(HomekitCommandExtension.class); - private Homekit homekit; + + private @NonNullByDefault({}) Homekit homekit; public HomekitCommandExtension() { super("homekit", "Interact with the HomeKit integration."); @@ -59,7 +62,7 @@ public void execute(String[] args, Console console) { case SUBCMD_ALLOW_UNAUTHENTICATED: if (args.length > 1) { - boolean allow = Boolean.valueOf(args[1]); + boolean allow = Boolean.parseBoolean(args[1]); allowUnauthenticatedHomekitRequests(allow, console); } else { console.println("true/false is required as an argument"); @@ -110,10 +113,6 @@ public void setHomekit(Homekit homekit) { this.homekit = homekit; } - public void unsetHomekit(Homekit homekit) { - this.homekit = null; - } - private void clearHomekitPairings(Console console) { homekit.clearHomekitPairings(); console.println("Cleared HomeKit pairings"); @@ -125,7 +124,7 @@ private void allowUnauthenticatedHomekitRequests(boolean allow, Console console) } private void listAccessories(Console console) { - homekit.getAccessories().stream().forEach(v -> { + homekit.getAccessories().forEach(v -> { try { console.println(v.getId() + " " + v.getName().get()); } catch (InterruptedException | ExecutionException e) { @@ -144,9 +143,7 @@ private void printAccessory(String id, Console console) { v.getServices().forEach(s -> { console.println(" Service Type: " + s.getType()); console.println(" Characteristics: "); - s.getCharacteristics().forEach(c -> { - console.println(" : " + c.getClass()); - }); + s.getCharacteristics().forEach(c -> console.println(" : " + c.getClass())); }); console.println(""); } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandType.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandType.java index 0c48aad8908a2..6b6b4ab72fd4b 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandType.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandType.java @@ -26,5 +26,5 @@ public enum HomekitCommandType { HUE_COMMAND, SATURATION_COMMAND, BRIGHTNESS_COMMAND, - ON_COMMAND; + ON_COMMAND } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitException.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitException.java index 71c722f01f26e..0815bfb03f7b7 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitException.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitException.java @@ -12,11 +12,14 @@ */ package org.openhab.io.homekit.internal; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link HomekitException} class defines an exception for handling HomekitException * * @author Jan N. Klug - Initial contribution */ +@NonNullByDefault public class HomekitException extends Exception { private static final long serialVersionUID = -8178227920946730286L; diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitImpl.java index 85478165a95ad..a195bf28efd9e 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitImpl.java @@ -14,7 +14,6 @@ import java.io.IOException; import java.net.InetAddress; -import java.net.UnknownHostException; import java.security.InvalidAlgorithmParameterException; import java.util.ArrayList; import java.util.List; @@ -30,7 +29,6 @@ import org.eclipse.smarthome.core.items.ItemRegistry; import org.eclipse.smarthome.core.items.MetadataRegistry; import org.eclipse.smarthome.core.net.NetworkAddressService; -import org.eclipse.smarthome.core.storage.Storage; import org.eclipse.smarthome.core.storage.StorageService; import org.openhab.io.homekit.Homekit; import org.osgi.framework.Constants; @@ -59,9 +57,7 @@ ConfigurableService.SERVICE_PROPERTY_LABEL + "=HomeKit Integration", "port:Integer=9123" }) @NonNullByDefault public class HomekitImpl implements Homekit { - private final Logger logger = LoggerFactory.getLogger(HomekitImpl.class); - private final Storage storage; private final NetworkAddressService networkAddressService; private final HomekitChangeListener changeListener; @@ -69,7 +65,7 @@ public class HomekitImpl implements Homekit { private @Nullable InetAddress networkInterface; private @Nullable HomekitServer homekitServer; private @Nullable HomekitRoot bridge; - private @Nullable HomekitAuthInfoImpl authInfo; + private final HomekitAuthInfoImpl authInfo; private final ScheduledExecutorService scheduler = ThreadPoolManager .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON); @@ -78,14 +74,14 @@ public class HomekitImpl implements Homekit { public HomekitImpl(@Reference StorageService storageService, @Reference ItemRegistry itemRegistry, @Reference NetworkAddressService networkAddressService, Map config, @Reference MetadataRegistry metadataRegistry) throws IOException, InvalidAlgorithmParameterException { - this.storage = storageService.getStorage("homekit"); this.networkAddressService = networkAddressService; this.settings = processConfig(config); this.changeListener = new HomekitChangeListener(itemRegistry, settings, metadataRegistry, storageService); + authInfo = new HomekitAuthInfoImpl(storageService.getStorage(HomekitAuthInfoImpl.STORAGE_KEY), settings.pin); startHomekitServer(); } - private HomekitSettings processConfig(Map config) throws UnknownHostException { + private HomekitSettings processConfig(Map config) { HomekitSettings settings = (new Configuration(config)).as(HomekitSettings.class); settings.process(); if (settings.networkInterface == null) { @@ -109,14 +105,13 @@ protected synchronized void modified(Map config) { stopBridge(); startBridge(); } - } catch (IOException | InvalidAlgorithmParameterException e) { + } catch (IOException e) { logger.warn("Could not initialize HomeKit connector: {}", e.getMessage()); - return; } } private void stopBridge() { - final HomekitRoot bridge = this.bridge; + final @Nullable HomekitRoot bridge = this.bridge; if (bridge != null) { changeListener.unsetBridge(); bridge.stop(); @@ -124,10 +119,9 @@ private void stopBridge() { } } - private void startBridge() throws InvalidAlgorithmParameterException, IOException { - final HomekitServer homekitServer = this.homekitServer; + private void startBridge() throws IOException { + final @Nullable HomekitServer homekitServer = this.homekitServer; if (homekitServer != null && bridge == null) { - authInfo = new HomekitAuthInfoImpl(storage, settings.pin); final HomekitRoot bridge = homekitServer.createBridge(authInfo, settings.name, HomekitSettings.MANUFACTURER, HomekitSettings.MODEL, HomekitSettings.SERIAL_NUMBER, FrameworkUtil.getBundle(getClass()).getVersion().toString(), HomekitSettings.HARDWARE_REVISION); @@ -158,7 +152,7 @@ private void startBridge() throws InvalidAlgorithmParameterException, IOExceptio } } - private void startHomekitServer() throws InvalidAlgorithmParameterException, IOException { + private void startHomekitServer() throws IOException { if (homekitServer == null) { networkInterface = InetAddress.getByName(settings.networkInterface); homekitServer = new HomekitServer(networkInterface, settings.port); @@ -169,7 +163,7 @@ private void startHomekitServer() throws InvalidAlgorithmParameterException, IOE } private void stopHomekitServer() { - final HomekitServer homekit = this.homekitServer; + final @Nullable HomekitServer homekit = this.homekitServer; if (homekit != null) { if (bridge != null) { stopBridge(); @@ -188,7 +182,7 @@ protected void deactivate() { @Override public void refreshAuthInfo() throws IOException { - final HomekitRoot bridge = this.bridge; + final @Nullable HomekitRoot bridge = this.bridge; if (bridge != null) { bridge.refreshAuthInfo(); } @@ -196,7 +190,7 @@ public void refreshAuthInfo() throws IOException { @Override public void allowUnauthenticatedRequests(boolean allow) { - final HomekitRoot bridge = this.bridge; + final @Nullable HomekitRoot bridge = this.bridge; if (bridge != null) { bridge.allowUnauthenticatedRequests(allow); } @@ -204,16 +198,14 @@ public void allowUnauthenticatedRequests(boolean allow) { @Override public List getAccessories() { - return new ArrayList(this.changeListener.getAccessories().values()); + return new ArrayList<>(this.changeListener.getAccessories().values()); } @Override public void clearHomekitPairings() { try { - if (authInfo != null) { - authInfo.clear(); - refreshAuthInfo(); - } + authInfo.clear(); + refreshAuthInfo(); } catch (Exception e) { logger.warn("Could not clear HomeKit pairings", e); } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitOHItemProxy.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitOHItemProxy.java index 842c2118543a1..b5b46d34de8fc 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitOHItemProxy.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitOHItemProxy.java @@ -53,12 +53,12 @@ public class HomekitOHItemProxy { private final Map commandCache = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduler = ThreadPoolManager .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON); - private @Nullable ScheduledFuture future; + private @NonNullByDefault({}) ScheduledFuture future; private HomekitDimmerMode dimmerMode = DIMMER_MODE_NORMAL; // delay, how long wait for further commands. in ms. private int delay = DEFAULT_DELAY; - public HomekitOHItemProxy(final Item item) { + public HomekitOHItemProxy(Item item) { this.item = item; } @@ -128,7 +128,7 @@ private void sendCommand() { commandCache.clear(); } - public synchronized void sendCommandProxy(final HomekitCommandType commandType, final State state) { + public synchronized void sendCommandProxy(HomekitCommandType commandType, State state) { commandCache.put(commandType, state); logger.trace("add command to command cache: item {}, command type {}, command state {}. cache state after: {}", this, commandType, state, commandCache); diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java index 44f9d2c0fbc48..cf825054af5d3 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java @@ -51,10 +51,10 @@ public class HomekitTaggedItem { private final HomekitAccessoryType homekitAccessoryType; // type of HomeKit characteristic, e.g. CurrentTemperature - private @Nullable HomekitCharacteristicType homekitCharacteristicType; + private HomekitCharacteristicType homekitCharacteristicType; // configuration attached to the openHAB Item, e.g. minValue, maxValue, valveType - private @Nullable Map configuration; + private final @Nullable Map configuration; // link to the groupItem if item is part of a group private @Nullable GroupItem parentGroupItem; @@ -78,14 +78,13 @@ public HomekitTaggedItem(HomekitOHItemProxy item, HomekitAccessoryType homekitAc } public HomekitTaggedItem(HomekitOHItemProxy item, HomekitAccessoryType homekitAccessoryType, - @Nullable HomekitCharacteristicType homekitCharacteristicType, - @Nullable Map configuration) { + HomekitCharacteristicType homekitCharacteristicType, @Nullable Map configuration) { this(item, homekitAccessoryType, configuration); this.homekitCharacteristicType = homekitCharacteristicType; } public HomekitTaggedItem(HomekitOHItemProxy item, HomekitAccessoryType homekitAccessoryType, - @Nullable HomekitCharacteristicType homekitCharacteristicType, @Nullable GroupItem parentGroup, + HomekitCharacteristicType homekitCharacteristicType, @Nullable GroupItem parentGroup, @Nullable Map configuration) { this(item, homekitAccessoryType, homekitCharacteristicType, configuration); this.parentGroupItem = parentGroup; @@ -100,7 +99,7 @@ public HomekitAccessoryType getAccessoryType() { return homekitAccessoryType; } - public @Nullable HomekitCharacteristicType getCharacteristicType() { + public HomekitCharacteristicType getCharacteristicType() { return homekitCharacteristicType; } @@ -123,7 +122,7 @@ public boolean isAccessory() { * root deviceGroup. */ public boolean isCharacteristic() { - return homekitCharacteristicType != null && homekitCharacteristicType != HomekitCharacteristicType.EMPTY; + return homekitCharacteristicType != HomekitCharacteristicType.EMPTY; } /** @@ -189,10 +188,10 @@ public boolean isMemberOfAccessoryGroup() { * @param expected class * @return value */ - @SuppressWarnings("unchecked") + @SuppressWarnings({ "null", "unchecked" }) public T getConfiguration(String key, T defaultValue) { if (configuration != null) { - final Object value = configuration.get(key); + final @Nullable Object value = configuration.get(key); if (value != null && value.getClass().equals(defaultValue.getClass())) { return (T) value; } @@ -216,11 +215,11 @@ public double getConfigurationAsDouble(String key, double defaultValue) { */ private void parseConfiguration() { if (configuration != null) { - Object dimmerModeConfig = configuration.get(DIMMER_MODE); + final @Nullable Object dimmerModeConfig = configuration.get(DIMMER_MODE); if (dimmerModeConfig instanceof String) { HomekitDimmerMode.valueOfTag((String) dimmerModeConfig).ifPresent(proxyItem::setDimmerMode); } - Object delayConfig = configuration.get(DELAY); + final @Nullable Object delayConfig = configuration.get(DELAY); if (delayConfig instanceof Number) { proxyItem.setDelay(((Number) delayConfig).intValue()); } @@ -250,7 +249,7 @@ private int calculateId(Item item) { } public String toString() { - return "Item:" + proxyItem + " HomeKit type:" + homekitAccessoryType + " HomeKit characteristic:" + return "Item:" + proxyItem.getItem() + " HomeKit type:" + homekitAccessoryType + " HomeKit characteristic:" + homekitCharacteristicType; } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java index 7722d0bc54ad8..c9f75610da641 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java @@ -25,10 +25,12 @@ import javax.measure.Quantity; import javax.measure.Unit; -import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.items.Item; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.eclipse.smarthome.core.library.types.StringType; import org.eclipse.smarthome.core.library.unit.ImperialUnits; import org.eclipse.smarthome.core.library.unit.SIUnits; @@ -59,7 +61,7 @@ abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory { private final List services; public AbstractHomekitAccessoryImpl(HomekitTaggedItem accessory, List characteristics, - HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { + HomekitAccessoryUpdater updater, HomekitSettings settings) { this.characteristics = characteristics; this.accessory = accessory; this.updater = updater; @@ -67,6 +69,7 @@ public AbstractHomekitAccessoryImpl(HomekitTaggedItem accessory, List getCharacteristic(HomekitCharacteristicType type) { return characteristics.stream().filter(c -> c.getCharacteristicType() == type).findAny(); } @@ -122,6 +125,7 @@ protected HomekitSettings getSettings() { return settings; } + @NonNullByDefault protected void subscribe(HomekitCharacteristicType characteristicType, HomekitCharacteristicChangeCallback callback) { final Optional characteristic = getCharacteristic(characteristicType); @@ -132,6 +136,7 @@ protected void subscribe(HomekitCharacteristicType characteristicType, } } + @NonNullByDefault protected void unsubscribe(HomekitCharacteristicType characteristicType) { final Optional characteristic = getCharacteristic(characteristicType); if (characteristic.isPresent()) { @@ -146,7 +151,7 @@ protected void unsubscribe(HomekitCharacteristicType characteristicType) { if (taggedItem.isPresent()) { final State state = taggedItem.get().getItem().getStateAs(type); if (state != null) { - return (T) state.as(type); + return state.as(type); } } logger.debug("State for characteristic {} at accessory {} cannot be retrieved.", characteristic, @@ -154,21 +159,23 @@ protected void unsubscribe(HomekitCharacteristicType characteristicType) { return null; } - @SuppressWarnings("unchecked") - protected @Nullable T getItem(HomekitCharacteristicType characteristic, Class type) { + @NonNullByDefault + protected Optional getItem(HomekitCharacteristicType characteristic, Class type) { final Optional taggedItem = getCharacteristic(characteristic); if (taggedItem.isPresent()) { - if (type.isInstance(taggedItem.get().getItem())) - return (T) taggedItem.get().getItem(); - else + final Item item = taggedItem.get().getItem(); + if (type.isInstance(item)) { + return Optional.of((T) item); + } else { logger.warn("Unsupported item type for characteristic {} at accessory {}. Expected {}, got {}", - characteristic, accessory.getItem().getLabel(), type, taggedItem.get().getItem().getClass()); + characteristic, accessory.getItem().getName(), type, taggedItem.get().getItem().getClass()); + } } else { logger.warn("Mandatory characteristic {} not found at accessory {}. ", characteristic, - accessory.getItem().getLabel()); + accessory.getItem().getName()); } - return null; + return Optional.empty(); } /** @@ -181,7 +188,8 @@ protected void unsubscribe(HomekitCharacteristicType characteristicType) { * @param expected type * @return configuration value */ - protected T getAccessoryConfiguration(@NonNull String key, @NonNull T defaultValue) { + @NonNullByDefault + protected T getAccessoryConfiguration(String key, T defaultValue) { return accessory.getConfiguration(key, defaultValue); } @@ -196,10 +204,11 @@ protected T getAccessoryConfiguration(@NonNull String key, @NonNull T defaul * @param expected type * @return configuration value */ - protected T getAccessoryConfiguration(@NonNull HomekitCharacteristicType characteristicType, - @NonNull String key, @NonNull T defaultValue) { - final Optional characteristic = getCharacteristic(characteristicType); - return characteristic.isPresent() ? characteristic.get().getConfiguration(key, defaultValue) : defaultValue; + @NonNullByDefault + protected T getAccessoryConfiguration(HomekitCharacteristicType characteristicType, String key, + T defaultValue) { + return getCharacteristic(characteristicType) + .map(homekitTaggedItem -> homekitTaggedItem.getConfiguration(key, defaultValue)).orElse(defaultValue); } /** @@ -210,6 +219,7 @@ protected T getAccessoryConfiguration(@NonNull HomekitCharacteristicType cha * @param characteristicType characteristicType to identify item * @param map mapping to update */ + @NonNullByDefault protected void updateMapping(HomekitCharacteristicType characteristicType, Map map) { getCharacteristic(characteristicType).ifPresent(c -> { final Map configuration = c.getConfiguration(); @@ -232,8 +242,9 @@ protected void updateMapping(HomekitCharacteristicType characteristicType, Map type of the result derived from * @return key for the value */ - protected T getKeyFromMapping(final HomekitCharacteristicType characteristicType, Map mapping, - final T defaultValue) { + @NonNullByDefault + protected T getKeyFromMapping(HomekitCharacteristicType characteristicType, Map mapping, + T defaultValue) { final Optional c = getCharacteristic(characteristicType); if (c.isPresent()) { final State state = c.get().getItem().getState(); @@ -253,23 +264,36 @@ protected T getKeyFromMapping(final HomekitCharacteristicType characteristic return defaultValue; } + @NonNullByDefault protected void addCharacteristic(HomekitTaggedItem characteristic) { characteristics.add(characteristic); } + @NonNullByDefault private > double convertAndRound(double value, Unit from, Unit to) { double rawValue = from == to ? value : from.getConverterTo(to).convert(value); return new BigDecimal(rawValue).setScale(1, RoundingMode.HALF_UP).doubleValue(); } + @NonNullByDefault protected double convertToCelsius(double degrees) { return convertAndRound(degrees, getSettings().useFahrenheitTemperature ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS, SIUnits.CELSIUS); } + @NonNullByDefault protected double convertFromCelsius(double degrees) { return convertAndRound(degrees, getSettings().useFahrenheitTemperature ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT, ImperialUnits.FAHRENHEIT); } + + @NonNullByDefault + protected BooleanItemReader createBooleanReader(HomekitCharacteristicType characteristicType, + OnOffType trueOnOffValue, OpenClosedType trueOpenClosedValue) throws IncompleteAccessoryException { + return new BooleanItemReader( + getItem(characteristicType, GenericItem.class) + .orElseThrow(() -> new IncompleteAccessoryException(characteristicType)), + trueOnOffValue, trueOpenClosedValue); + } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/BooleanItemReader.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/BooleanItemReader.java index 3c64b7b832659..2bdc5cd0a978a 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/BooleanItemReader.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/BooleanItemReader.java @@ -12,6 +12,7 @@ */ package org.openhab.io.homekit.internal.accessories; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.core.items.GroupItem; import org.eclipse.smarthome.core.items.Item; import org.eclipse.smarthome.core.library.items.ContactItem; @@ -30,11 +31,11 @@ * @author Tim Harper - Initial contribution * */ +@NonNullByDefault public class BooleanItemReader { private final Item item; private final OnOffType trueOnOffValue; private final OpenClosedType trueOpenClosedValue; - private final Logger logger = LoggerFactory.getLogger(BooleanItemReader.class); /** @@ -53,8 +54,8 @@ public class BooleanItemReader { } } - Boolean getValue() { - State state = item.getState(); + boolean getValue() { + final State state = item.getState(); if (state instanceof OnOffType) { return state.equals(trueOnOffValue); } else if (state instanceof OpenClosedType) { @@ -62,15 +63,22 @@ Boolean getValue() { } else if (state instanceof StringType) { return state.toString().equalsIgnoreCase("Open") || state.toString().equalsIgnoreCase("Opened"); } else { - return null; + logger.debug("Unexpected item state, returning false. Item {}, State {}", item.getName(), state); + return false; } } + private OnOffType getOffValue(OnOffType onValue) { + return onValue == OnOffType.ON ? OnOffType.OFF : OnOffType.ON; + } + void setValue(Boolean value) { if (item instanceof SwitchItem) { - ((SwitchItem) item).send(OnOffType.from(value)); + ((SwitchItem) item).send(value ? trueOnOffValue : getOffValue(trueOnOffValue)); } else if (item instanceof GroupItem) { - ((GroupItem) item).send(OnOffType.from(value)); + ((GroupItem) item).send(value ? trueOnOffValue : getOffValue(trueOnOffValue)); + } else { + logger.debug("Cannot set value {} for item {}. Only Switch and Group items are supported.", value, item); } } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java index 46b294a01f6ba..2d30d8d3fb794 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java @@ -168,7 +168,7 @@ public class HomekitAccessoryFactory { public static HomekitAccessory create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry, HomekitAccessoryUpdater updater, HomekitSettings settings) throws HomekitException { final HomekitAccessoryType accessoryType = taggedItem.getAccessoryType(); - logger.trace("Constructing {} of accessoryType {}", taggedItem.getName(), accessoryType.getTag()); + logger.trace("Constructing {} of accessory type {}", taggedItem.getName(), accessoryType.getTag()); final List requiredCharacteristics = getMandatoryCharacteristics(taggedItem, metadataRegistry); final HomekitCharacteristicType[] mandatoryCharacteristics = MANDATORY_CHARACTERISTICS.get(accessoryType); @@ -178,10 +178,8 @@ public static HomekitAccessory create(HomekitTaggedItem taggedItem, MetadataRegi throw new HomekitException("Missing mandatory characteristics"); } AbstractHomekitAccessoryImpl accessoryImpl; - try { - @Nullable - final Class accessoryImplClass = SERVICE_IMPL_MAP + final @Nullable Class accessoryImplClass = SERVICE_IMPL_MAP .get(accessoryType); if (accessoryImplClass != null) { accessoryImpl = accessoryImplClass @@ -211,11 +209,9 @@ public static HomekitAccessory create(HomekitTaggedItem taggedItem, MetadataRegi public static List> getAccessoryTypes(Item item, MetadataRegistry metadataRegistry) { final List> accessories = new ArrayList<>(); - Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID())); + final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID())); boolean legacyMode = metadata == null; String[] tags = !legacyMode ? metadata.getValue().split(",") : item.getTags().toArray(new String[0]); // fallback - - logger.trace("item {} meta data {} tags {} ", item.getName(), metadata, tags); for (String tag : tags) { final String[] meta = tag.split("\\."); Optional accessoryType = HomekitAccessoryType.valueOfTag(meta[0].trim()); @@ -246,7 +242,7 @@ public static List> getAc } public static @Nullable Map getItemConfiguration(Item item, MetadataRegistry metadataRegistry) { - Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID())); + final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID())); return metadata != null ? metadata.getConfiguration() : null; } @@ -261,7 +257,7 @@ public static List> getAc public static List getAccessoryGroups(Item item, ItemRegistry itemRegistry, MetadataRegistry metadataRegistry) { return item.getGroupNames().stream().flatMap(name -> { - Item groupItem = itemRegistry.get(name); + final @Nullable Item groupItem = itemRegistry.get(name); if ((groupItem instanceof GroupItem) && ((GroupItem) groupItem).getBaseItem() == null) { return Stream.of((GroupItem) groupItem); } else { @@ -280,8 +276,6 @@ public static List getAccessoryGroups(Item item, ItemRegistry itemReg */ private static List getMandatoryCharacteristics(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry) { - logger.trace("get mandatory characteristics for item {}: isGroup? {}, isMember? {}", taggedItem.getName(), - taggedItem.isGroup(), taggedItem.isMemberOfAccessoryGroup()); List collectedCharacteristics = new ArrayList<>(); if (taggedItem.isGroup()) { for (Item item : ((GroupItem) taggedItem.getItem()).getAllMembers()) { @@ -290,6 +284,8 @@ private static List getMandatoryCharacteristics(HomekitTagged } else { addMandatoryCharacteristics(taggedItem, collectedCharacteristics, taggedItem.getItem(), metadataRegistry); } + logger.trace("Mandatory characteristics for item {} characteristics {}", taggedItem.getName(), + collectedCharacteristics); return collectedCharacteristics; } @@ -305,7 +301,6 @@ private static List getMandatoryCharacteristics(HomekitTagged * @param item current item * @param metadataRegistry meta date registry */ - @SuppressWarnings("null") private static void addMandatoryCharacteristics(HomekitTaggedItem mainItem, List characteristics, Item item, MetadataRegistry metadataRegistry) { // get list of mandatory characteristics @@ -353,8 +348,8 @@ private static void addMandatoryCharacteristics(HomekitTaggedItem mainItem, List * @param accessory accessory * @param metadataRegistry metadata registry */ - private static void addOptionalCharacteristics(final HomekitTaggedItem taggedItem, - AbstractHomekitAccessoryImpl accessory, MetadataRegistry metadataRegistry) { + private static void addOptionalCharacteristics(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory, + MetadataRegistry metadataRegistry) { Map characteristics = getOptionalCharacteristics( accessory.getRootAccessory(), metadataRegistry); Service service = accessory.getPrimaryService(); @@ -363,7 +358,6 @@ private static void addOptionalCharacteristics(final HomekitTaggedItem taggedIte // an accessory can have multiple optional characteristics. iterate over them. characteristics.forEach((type, item) -> { try { - logger.trace("adding optional characteristic: {} for item {}", type.getTag(), item.getName()); // check whether a proxyItem already exists, if not create one. final HomekitOHItemProxy proxyItem = proxyItems.computeIfAbsent(item.getUID(), k -> new HomekitOHItemProxy(item)); @@ -376,11 +370,10 @@ private static void addOptionalCharacteristics(final HomekitTaggedItem taggedIte // find the corresponding add method at service and call it. service.getClass().getMethod("addOptionalCharacteristic", characteristic.getClass()).invoke(service, characteristic); - accessory.addCharacteristic(optionalItem); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | HomekitException e) { - logger.warn("Not supported optional HomeKit characteristic. Service type {}, characteristic type {}", - service.getType(), type.getTag(), e); + logger.warn("Unsupported optional HomeKit characteristic: service type {}, characteristic type {}", + service.getType(), type.getTag()); } }); } @@ -392,11 +385,8 @@ private static void addOptionalCharacteristics(final HomekitTaggedItem taggedIte * @param metadataRegistry OH metadata registry * @return a map with characteristics and corresponding OH items */ - @SuppressWarnings("null") private static Map getOptionalCharacteristics(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry) { - logger.trace("get optional characteristics for item {}: isGroup? {}, isMember? {}", taggedItem.getName(), - taggedItem.isGroup(), taggedItem.isMemberOfAccessoryGroup()); Map characteristicItems = new HashMap<>(); if (taggedItem.isGroup()) { GroupItem groupItem = (GroupItem) taggedItem.getItem(); @@ -419,7 +409,8 @@ private static Map getOptionalCharacteri (GenericItem) taggedItem.getItem())); } } - logger.trace("characteristics for {} = {}", taggedItem.getName(), characteristicItems); + logger.trace("Optional characteristics for item {} characteristics {}", taggedItem.getName(), + characteristicItems); return Collections.unmodifiableMap(characteristicItems); } @@ -430,10 +421,9 @@ private static Map getOptionalCharacteri * @param characteristic characteristic * @return true if characteristic is mandatory, false if not mandatory */ - @SuppressWarnings("null") private static boolean isMandatoryCharacteristic(HomekitAccessoryType accessory, HomekitCharacteristicType characteristic) { - return MANDATORY_CHARACTERISTICS.get(accessory) != null + return MANDATORY_CHARACTERISTICS.containsKey(accessory) && Arrays.asList(MANDATORY_CHARACTERISTICS.get(accessory)).contains(characteristic); } @@ -443,21 +433,19 @@ private static boolean isMandatoryCharacteristic(HomekitAccessoryType accessory, * @param accessory accessory * @return true if accessory has not characteristic. */ - @SuppressWarnings("null") private static boolean isRootAccessory(Entry accessory) { return ((accessory.getValue() == null) || (accessory.getValue() == EMPTY)); } /** * check whether it is legacy characteristic and return new name in such case. otherwise return the input parameter - * unchangec. + * unchanged. * * @param characteristicType characteristic to check * @return new characteristic type */ - @SuppressWarnings("null") - private static HomekitCharacteristicType legacyCheck(final HomekitCharacteristicType characteristicType) { - if (LEGACY_CHARACTERISTICS_MAPPING.get(characteristicType) != null) + private static HomekitCharacteristicType legacyCheck(HomekitCharacteristicType characteristicType) { + if (LEGACY_CHARACTERISTICS_MAPPING.containsKey(characteristicType)) return LEGACY_CHARACTERISTICS_MAPPING.get(characteristicType); return characteristicType; } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCarbonDioxideSensorImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCarbonDioxideSensorImpl.java index 751612da57cbb..d92ec76a08708 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCarbonDioxideSensorImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCarbonDioxideSensorImpl.java @@ -12,14 +12,14 @@ */ package org.openhab.io.homekit.internal.accessories; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.CARBON_DIOXIDE_DETECTED_STATE; + import java.util.List; import java.util.concurrent.CompletableFuture; -import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; -import org.openhab.io.homekit.internal.HomekitCharacteristicType; import org.openhab.io.homekit.internal.HomekitSettings; import org.openhab.io.homekit.internal.HomekitTaggedItem; @@ -34,36 +34,31 @@ */ public class HomekitCarbonDioxideSensorImpl extends AbstractHomekitAccessoryImpl implements CarbonDioxideSensorAccessory { - private final BooleanItemReader carbonDioxideDetectedReader; public HomekitCarbonDioxideSensorImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); - this.carbonDioxideDetectedReader = new BooleanItemReader( - getItem(HomekitCharacteristicType.CARBON_DIOXIDE_DETECTED_STATE, GenericItem.class), OnOffType.ON, + carbonDioxideDetectedReader = createBooleanReader(CARBON_DIOXIDE_DETECTED_STATE, OnOffType.ON, OpenClosedType.OPEN); getServices().add(new CarbonDioxideSensorService(this)); } @Override public CompletableFuture getCarbonDioxideDetectedState() { - Boolean state = this.carbonDioxideDetectedReader.getValue(); - if (state == null) { - return CompletableFuture.completedFuture(CarbonDioxideDetectedEnum.NORMAL); - } return CompletableFuture - .completedFuture(state ? CarbonDioxideDetectedEnum.ABNORMAL : CarbonDioxideDetectedEnum.NORMAL); + .completedFuture(carbonDioxideDetectedReader.getValue() ? CarbonDioxideDetectedEnum.ABNORMAL + : CarbonDioxideDetectedEnum.NORMAL); } @Override public void subscribeCarbonDioxideDetectedState(HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.CARBON_DIOXIDE_DETECTED_STATE, callback); + subscribe(CARBON_DIOXIDE_DETECTED_STATE, callback); } @Override public void unsubscribeCarbonDioxideDetectedState() { - unsubscribe(HomekitCharacteristicType.CARBON_DIOXIDE_DETECTED_STATE); + unsubscribe(CARBON_DIOXIDE_DETECTED_STATE); } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCarbonMonoxideSensorImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCarbonMonoxideSensorImpl.java index e6b75d8f9dc37..5625161cbd872 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCarbonMonoxideSensorImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCarbonMonoxideSensorImpl.java @@ -12,14 +12,14 @@ */ package org.openhab.io.homekit.internal.accessories; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.CARBON_MONOXIDE_DETECTED_STATE; + import java.util.List; import java.util.concurrent.CompletableFuture; -import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; -import org.openhab.io.homekit.internal.HomekitCharacteristicType; import org.openhab.io.homekit.internal.HomekitSettings; import org.openhab.io.homekit.internal.HomekitTaggedItem; @@ -34,36 +34,31 @@ */ public class HomekitCarbonMonoxideSensorImpl extends AbstractHomekitAccessoryImpl implements CarbonMonoxideSensorAccessory { - private final BooleanItemReader carbonMonoxideDetectedReader; public HomekitCarbonMonoxideSensorImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); - this.carbonMonoxideDetectedReader = new BooleanItemReader( - getItem(HomekitCharacteristicType.CARBON_MONOXIDE_DETECTED_STATE, GenericItem.class), OnOffType.ON, + carbonMonoxideDetectedReader = createBooleanReader(CARBON_MONOXIDE_DETECTED_STATE, OnOffType.ON, OpenClosedType.OPEN); getServices().add(new CarbonMonoxideSensorService(this)); } @Override public CompletableFuture getCarbonMonoxideDetectedState() { - Boolean state = this.carbonMonoxideDetectedReader.getValue(); - if (state == null) { - return CompletableFuture.completedFuture(null); - } return CompletableFuture - .completedFuture(state ? CarbonMonoxideDetectedEnum.ABNORMAL : CarbonMonoxideDetectedEnum.NORMAL); + .completedFuture(carbonMonoxideDetectedReader.getValue() ? CarbonMonoxideDetectedEnum.ABNORMAL + : CarbonMonoxideDetectedEnum.NORMAL); } @Override public void subscribeCarbonMonoxideDetectedState(HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.CARBON_MONOXIDE_DETECTED_STATE, callback); + subscribe(CARBON_MONOXIDE_DETECTED_STATE, callback); } @Override public void unsubscribeCarbonMonoxideDetectedState() { - unsubscribe(HomekitCharacteristicType.CARBON_MONOXIDE_DETECTED_STATE); + unsubscribe(CARBON_MONOXIDE_DETECTED_STATE); } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCharacteristicFactory.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCharacteristicFactory.java index 64c3f946a07f9..d9a7a04bede64 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCharacteristicFactory.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCharacteristicFactory.java @@ -136,10 +136,10 @@ public class HomekitCharacteristicFactory { * @param updater update to keep OH item and HomeKit characteristic in sync * @return HomeKit characteristic */ - public static Characteristic createCharacteristic(final HomekitTaggedItem item, HomekitAccessoryUpdater updater) + public static Characteristic createCharacteristic(HomekitTaggedItem item, HomekitAccessoryUpdater updater) throws HomekitException { final @Nullable HomekitCharacteristicType type = item.getCharacteristicType(); - logger.trace("createCharacteristic, type {} item {}", type, item); + logger.trace("CreateCharacteristic, type {} item {}", type, item); if (optional.containsKey(type)) { return optional.get(type).apply(item, updater); } @@ -151,8 +151,7 @@ public static Characteristic createCharacteristic(final HomekitTaggedItem item, // METHODS TO CREATE SINGLE CHARACTERISTIC FROM OH ITEM // supporting methods - @SuppressWarnings("null") - private static CompletableFuture getEnumFromItem(final HomekitTaggedItem item, + private static CompletableFuture getEnumFromItem(HomekitTaggedItem item, T offEnum, T onEnum, T defaultEnum) { final State state = item.getItem().getState(); if (state instanceof OnOffType) { @@ -160,7 +159,7 @@ private static CompletableFuture getEnumFromIt } else if (state instanceof OpenClosedType) { return CompletableFuture.completedFuture(state.equals(OpenClosedType.CLOSED) ? offEnum : onEnum); } else if (state instanceof DecimalType) { - return CompletableFuture.completedFuture(state.as(DecimalType.class).intValue() == 0 ? offEnum : onEnum); + return CompletableFuture.completedFuture(((DecimalType) state).intValue() == 0 ? offEnum : onEnum); } else if (state instanceof UnDefType) { return CompletableFuture.completedFuture(defaultEnum); } @@ -170,7 +169,7 @@ private static CompletableFuture getEnumFromIt return CompletableFuture.completedFuture(defaultEnum); } - private static void setValueFromEnum(final HomekitTaggedItem taggedItem, CharacteristicEnum value, + private static void setValueFromEnum(HomekitTaggedItem taggedItem, CharacteristicEnum value, CharacteristicEnum offEnum, CharacteristicEnum onEnum) { if (taggedItem.getItem() instanceof SwitchItem) { if (value.equals(offEnum)) { @@ -189,14 +188,13 @@ private static void setValueFromEnum(final HomekitTaggedItem taggedItem, Charact } } - @SuppressWarnings("null") - private static int getIntFromItem(final HomekitTaggedItem taggedItem) { + private static int getIntFromItem(HomekitTaggedItem taggedItem) { int value = 0; final State state = taggedItem.getItem().getState(); if (state instanceof PercentType) { - value = state.as(PercentType.class).intValue(); + value = ((PercentType) state).intValue(); } else if (state instanceof DecimalType) { - value = state.as(DecimalType.class).intValue(); + value = ((DecimalType) state).intValue(); } else if (state instanceof UnDefType) { logger.debug("Item state {} is UNDEF {}.", state, taggedItem.getName()); } else { @@ -207,11 +205,11 @@ private static int getIntFromItem(final HomekitTaggedItem taggedItem) { return value; } - private static Supplier> getIntSupplier(final HomekitTaggedItem taggedItem) { + private static Supplier> getIntSupplier(HomekitTaggedItem taggedItem) { return () -> CompletableFuture.completedFuture(getIntFromItem(taggedItem)); } - private static ExceptionalConsumer setIntConsumer(final HomekitTaggedItem taggedItem) { + private static ExceptionalConsumer setIntConsumer(HomekitTaggedItem taggedItem) { return (value) -> { if (taggedItem.getItem() instanceof NumberItem) { ((NumberItem) taggedItem.getItem()).send(new DecimalType(value)); @@ -222,7 +220,7 @@ private static ExceptionalConsumer setIntConsumer(final HomekitTaggedIt }; } - private static ExceptionalConsumer setPercentConsumer(final HomekitTaggedItem taggedItem) { + private static ExceptionalConsumer setPercentConsumer(HomekitTaggedItem taggedItem) { return (value) -> { if (taggedItem.getItem() instanceof NumberItem) { ((NumberItem) taggedItem.getItem()).send(new DecimalType(value)); @@ -235,14 +233,14 @@ private static ExceptionalConsumer setPercentConsumer(final HomekitTagg }; } - private static Supplier> getDoubleSupplier(final HomekitTaggedItem taggedItem) { + private static Supplier> getDoubleSupplier(HomekitTaggedItem taggedItem) { return () -> { - final DecimalType value = taggedItem.getItem().getStateAs(DecimalType.class); + final @Nullable DecimalType value = taggedItem.getItem().getStateAs(DecimalType.class); return CompletableFuture.completedFuture(value != null ? value.doubleValue() : 0.0); }; } - private static ExceptionalConsumer setDoubleConsumer(final HomekitTaggedItem taggedItem) { + private static ExceptionalConsumer setDoubleConsumer(HomekitTaggedItem taggedItem) { return (value) -> { if (taggedItem.getItem() instanceof NumberItem) { ((NumberItem) taggedItem.getItem()).send(new DecimalType(value)); @@ -253,19 +251,19 @@ private static ExceptionalConsumer setDoubleConsumer(final HomekitTagged }; } - protected static Consumer getSubscriber(final HomekitTaggedItem taggedItem, - final HomekitCharacteristicType key, final HomekitAccessoryUpdater updater) { + protected static Consumer getSubscriber(HomekitTaggedItem taggedItem, + HomekitCharacteristicType key, HomekitAccessoryUpdater updater) { return (callback) -> updater.subscribe((GenericItem) taggedItem.getItem(), key.getTag(), callback); } - protected static Runnable getUnsubscriber(final HomekitTaggedItem taggedItem, final HomekitCharacteristicType key, - final HomekitAccessoryUpdater updater) { + protected static Runnable getUnsubscriber(HomekitTaggedItem taggedItem, HomekitCharacteristicType key, + HomekitAccessoryUpdater updater) { return () -> updater.unsubscribe((GenericItem) taggedItem.getItem(), key.getTag()); } // create method for characteristic - private static StatusLowBatteryCharacteristic createStatusLowBatteryCharacteristic( - final HomekitTaggedItem taggedItem, final HomekitAccessoryUpdater updater) { + private static StatusLowBatteryCharacteristic createStatusLowBatteryCharacteristic(HomekitTaggedItem taggedItem, + HomekitAccessoryUpdater updater) { return new StatusLowBatteryCharacteristic( () -> getEnumFromItem(taggedItem, StatusLowBatteryEnum.NORMAL, StatusLowBatteryEnum.LOW, StatusLowBatteryEnum.NORMAL), @@ -273,16 +271,16 @@ private static StatusLowBatteryCharacteristic createStatusLowBatteryCharacterist getUnsubscriber(taggedItem, BATTERY_LOW_STATUS, updater)); } - private static StatusFaultCharacteristic createStatusFaultCharacteristic(final HomekitTaggedItem taggedItem, - final HomekitAccessoryUpdater updater) { + private static StatusFaultCharacteristic createStatusFaultCharacteristic(HomekitTaggedItem taggedItem, + HomekitAccessoryUpdater updater) { return new StatusFaultCharacteristic( () -> getEnumFromItem(taggedItem, StatusFaultEnum.NO_FAULT, StatusFaultEnum.GENERAL_FAULT, StatusFaultEnum.NO_FAULT), getSubscriber(taggedItem, FAULT_STATUS, updater), getUnsubscriber(taggedItem, FAULT_STATUS, updater)); } - private static StatusTamperedCharacteristic createStatusTamperedCharacteristic(final HomekitTaggedItem taggedItem, - final HomekitAccessoryUpdater updater) { + private static StatusTamperedCharacteristic createStatusTamperedCharacteristic(HomekitTaggedItem taggedItem, + HomekitAccessoryUpdater updater) { return new StatusTamperedCharacteristic( () -> getEnumFromItem(taggedItem, StatusTamperedEnum.NOT_TAMPERED, StatusTamperedEnum.TAMPERED, StatusTamperedEnum.NOT_TAMPERED), @@ -291,7 +289,7 @@ private static StatusTamperedCharacteristic createStatusTamperedCharacteristic(f } private static ObstructionDetectedCharacteristic createObstructionDetectedCharacteristic( - final HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { + HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new ObstructionDetectedCharacteristic( () -> CompletableFuture.completedFuture(taggedItem.getItem().getState() == OnOffType.ON || taggedItem.getItem().getState() == OpenClosedType.OPEN), @@ -299,7 +297,7 @@ private static ObstructionDetectedCharacteristic createObstructionDetectedCharac getUnsubscriber(taggedItem, OBSTRUCTION_STATUS, updater)); } - private static StatusActiveCharacteristic createStatusActiveCharacteristic(final HomekitTaggedItem taggedItem, + private static StatusActiveCharacteristic createStatusActiveCharacteristic(HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new StatusActiveCharacteristic( () -> CompletableFuture.completedFuture(taggedItem.getItem().getState() == OnOffType.ON @@ -307,7 +305,7 @@ private static StatusActiveCharacteristic createStatusActiveCharacteristic(final getSubscriber(taggedItem, ACTIVE_STATUS, updater), getUnsubscriber(taggedItem, ACTIVE_STATUS, updater)); } - private static NameCharacteristic createNameCharacteristic(final HomekitTaggedItem taggedItem, + private static NameCharacteristic createNameCharacteristic(HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new NameCharacteristic(() -> { final State state = taggedItem.getItem().getState(); @@ -315,71 +313,71 @@ private static NameCharacteristic createNameCharacteristic(final HomekitTaggedIt }); } - private static HoldPositionCharacteristic createHoldPositionCharacteristic(final HomekitTaggedItem taggedItem, + private static HoldPositionCharacteristic createHoldPositionCharacteristic(HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { - return new HoldPositionCharacteristic(OnOffType::from); + return new HoldPositionCharacteristic(value -> ((SwitchItem) taggedItem.getItem()).send(OnOffType.from(value))); } private static CarbonMonoxideLevelCharacteristic createCarbonMonoxideLevelCharacteristic( - final HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { + HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new CarbonMonoxideLevelCharacteristic(getDoubleSupplier(taggedItem), getSubscriber(taggedItem, CARBON_DIOXIDE_LEVEL, updater), getUnsubscriber(taggedItem, CARBON_DIOXIDE_LEVEL, updater)); } private static CarbonMonoxidePeakLevelCharacteristic createCarbonMonoxidePeakLevelCharacteristic( - final HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { + HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new CarbonMonoxidePeakLevelCharacteristic(getDoubleSupplier(taggedItem), getSubscriber(taggedItem, CARBON_DIOXIDE_PEAK_LEVEL, updater), getUnsubscriber(taggedItem, CARBON_DIOXIDE_PEAK_LEVEL, updater)); } - private static CarbonDioxideLevelCharacteristic createCarbonDioxideLevelCharacteristic( - final HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { + private static CarbonDioxideLevelCharacteristic createCarbonDioxideLevelCharacteristic(HomekitTaggedItem taggedItem, + HomekitAccessoryUpdater updater) { return new CarbonDioxideLevelCharacteristic(getDoubleSupplier(taggedItem), getSubscriber(taggedItem, CARBON_MONOXIDE_LEVEL, updater), getUnsubscriber(taggedItem, CARBON_MONOXIDE_LEVEL, updater)); } private static CarbonDioxidePeakLevelCharacteristic createCarbonDioxidePeakLevelCharacteristic( - final HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { + HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new CarbonDioxidePeakLevelCharacteristic(getDoubleSupplier(taggedItem), getSubscriber(taggedItem, CARBON_MONOXIDE_PEAK_LEVEL, updater), getUnsubscriber(taggedItem, CARBON_MONOXIDE_PEAK_LEVEL, updater)); } private static CurrentHorizontalTiltAngleCharacteristic createCurrentHorizontalTiltAngleCharacteristic( - final HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { + HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new CurrentHorizontalTiltAngleCharacteristic(getIntSupplier(taggedItem), getSubscriber(taggedItem, CURRENT_HORIZONTAL_TILT_ANGLE, updater), getUnsubscriber(taggedItem, CURRENT_HORIZONTAL_TILT_ANGLE, updater)); } private static CurrentVerticalTiltAngleCharacteristic createCurrentVerticalTiltAngleCharacteristic( - final HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { + HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new CurrentVerticalTiltAngleCharacteristic(getIntSupplier(taggedItem), getSubscriber(taggedItem, CURRENT_VERTICAL_TILT_ANGLE, updater), getUnsubscriber(taggedItem, CURRENT_VERTICAL_TILT_ANGLE, updater)); } private static TargetHorizontalTiltAngleCharacteristic createTargetHorizontalTiltAngleCharacteristic( - final HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { + HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new TargetHorizontalTiltAngleCharacteristic(getIntSupplier(taggedItem), setIntConsumer(taggedItem), getSubscriber(taggedItem, TARGET_HORIZONTAL_TILT_ANGLE, updater), getUnsubscriber(taggedItem, TARGET_HORIZONTAL_TILT_ANGLE, updater)); } private static TargetVerticalTiltAngleCharacteristic createTargetVerticalTiltAngleCharacteristic( - final HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { + HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new TargetVerticalTiltAngleCharacteristic(getIntSupplier(taggedItem), setIntConsumer(taggedItem), getSubscriber(taggedItem, TARGET_HORIZONTAL_TILT_ANGLE, updater), getUnsubscriber(taggedItem, TARGET_HORIZONTAL_TILT_ANGLE, updater)); } - private static HueCharacteristic createHueCharacteristic(final HomekitTaggedItem taggedItem, + private static HueCharacteristic createHueCharacteristic(HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new HueCharacteristic(() -> { - Double value = 0.0; + double value = 0.0; State state = taggedItem.getItem().getState(); if (state instanceof HSBType) { value = ((HSBType) state).getHue().doubleValue(); @@ -395,7 +393,7 @@ private static HueCharacteristic createHueCharacteristic(final HomekitTaggedItem }, getSubscriber(taggedItem, HUE, updater), getUnsubscriber(taggedItem, HUE, updater)); } - private static BrightnessCharacteristic createBrightnessCharacteristic(final HomekitTaggedItem taggedItem, + private static BrightnessCharacteristic createBrightnessCharacteristic(HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new BrightnessCharacteristic(() -> { int value = 0; @@ -417,10 +415,10 @@ private static BrightnessCharacteristic createBrightnessCharacteristic(final Hom }, getSubscriber(taggedItem, BRIGHTNESS, updater), getUnsubscriber(taggedItem, BRIGHTNESS, updater)); } - private static SaturationCharacteristic createSaturationCharacteristic(final HomekitTaggedItem taggedItem, + private static SaturationCharacteristic createSaturationCharacteristic(HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new SaturationCharacteristic(() -> { - Double value = 0.0; + double value = 0.0; State state = taggedItem.getItem().getState(); if (state instanceof HSBType) { value = ((HSBType) state).getSaturation().doubleValue(); @@ -439,17 +437,18 @@ private static SaturationCharacteristic createSaturationCharacteristic(final Hom }, getSubscriber(taggedItem, SATURATION, updater), getUnsubscriber(taggedItem, SATURATION, updater)); } - private static ColorTemperatureCharacteristic createColorTemperatureCharacteristic( - final HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { + private static ColorTemperatureCharacteristic createColorTemperatureCharacteristic(HomekitTaggedItem taggedItem, + HomekitAccessoryUpdater updater) { return new ColorTemperatureCharacteristic(getIntSupplier(taggedItem), setIntConsumer(taggedItem), getSubscriber(taggedItem, COLOR_TEMPERATURE, updater), getUnsubscriber(taggedItem, COLOR_TEMPERATURE, updater)); } - private static CurrentFanStateCharacteristic createCurrentFanStateCharacteristic(final HomekitTaggedItem taggedItem, + private static CurrentFanStateCharacteristic createCurrentFanStateCharacteristic(HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new CurrentFanStateCharacteristic(() -> { - final DecimalType value = taggedItem.getItem().getStateAs(DecimalType.class); + final @Nullable DecimalType value = taggedItem.getItem().getStateAs(DecimalType.class); + @Nullable CurrentFanStateEnum currentFanStateEnum = value != null ? CurrentFanStateEnum.fromCode(value.intValue()) : null; if (currentFanStateEnum == null) { @@ -460,10 +459,11 @@ private static CurrentFanStateCharacteristic createCurrentFanStateCharacteristic getUnsubscriber(taggedItem, CURRENT_FAN_STATE, updater)); } - private static TargetFanStateCharacteristic createTargetFanStateCharacteristic(final HomekitTaggedItem taggedItem, + private static TargetFanStateCharacteristic createTargetFanStateCharacteristic(HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new TargetFanStateCharacteristic(() -> { - final DecimalType value = taggedItem.getItem().getStateAs(DecimalType.class); + final @Nullable DecimalType value = taggedItem.getItem().getStateAs(DecimalType.class); + @Nullable TargetFanStateEnum targetFanStateEnum = value != null ? TargetFanStateEnum.fromCode(value.intValue()) : null; if (targetFanStateEnum == null) { @@ -481,8 +481,8 @@ private static TargetFanStateCharacteristic createTargetFanStateCharacteristic(f getUnsubscriber(taggedItem, TARGET_FAN_STATE, updater)); } - private static RotationDirectionCharacteristic createRotationDirectionCharacteristic( - final HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { + private static RotationDirectionCharacteristic createRotationDirectionCharacteristic(HomekitTaggedItem taggedItem, + HomekitAccessoryUpdater updater) { return new RotationDirectionCharacteristic( () -> getEnumFromItem(taggedItem, RotationDirectionEnum.CLOCKWISE, RotationDirectionEnum.COUNTER_CLOCKWISE, RotationDirectionEnum.CLOCKWISE), @@ -492,7 +492,7 @@ private static RotationDirectionCharacteristic createRotationDirectionCharacteri getUnsubscriber(taggedItem, ROTATION_DIRECTION, updater)); } - private static SwingModeCharacteristic createSwingModeCharacteristic(final HomekitTaggedItem taggedItem, + private static SwingModeCharacteristic createSwingModeCharacteristic(HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new SwingModeCharacteristic( () -> getEnumFromItem(taggedItem, SwingModeEnum.SWING_DISABLED, SwingModeEnum.SWING_ENABLED, @@ -503,7 +503,7 @@ private static SwingModeCharacteristic createSwingModeCharacteristic(final Homek } private static LockPhysicalControlsCharacteristic createLockPhysicalControlsCharacteristic( - final HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { + HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new LockPhysicalControlsCharacteristic( () -> getEnumFromItem(taggedItem, LockPhysicalControlsEnum.CONTROL_LOCK_DISABLED, LockPhysicalControlsEnum.CONTROL_LOCK_ENABLED, LockPhysicalControlsEnum.CONTROL_LOCK_DISABLED), @@ -512,18 +512,19 @@ private static LockPhysicalControlsCharacteristic createLockPhysicalControlsChar getSubscriber(taggedItem, LOCK_CONTROL, updater), getUnsubscriber(taggedItem, LOCK_CONTROL, updater)); } - private static RotationSpeedCharacteristic createRotationSpeedCharacteristic(final HomekitTaggedItem item, + private static RotationSpeedCharacteristic createRotationSpeedCharacteristic(HomekitTaggedItem item, HomekitAccessoryUpdater updater) { return new RotationSpeedCharacteristic(getIntSupplier(item), setPercentConsumer(item), getSubscriber(item, ROTATION_SPEED, updater), getUnsubscriber(item, ROTATION_SPEED, updater)); } - private static SetDurationCharacteristic createDurationCharacteristic(final HomekitTaggedItem taggedItem, + private static SetDurationCharacteristic createDurationCharacteristic(HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new SetDurationCharacteristic(() -> { int value = getIntFromItem(taggedItem); - if (value == 0) { // check for default duration - final Object duration = taggedItem.getConfiguration().get(HomekitValveImpl.CONFIG_DEFAULT_DURATION); + final @Nullable Map itemConfiguration = taggedItem.getConfiguration(); + if ((value == 0) && (itemConfiguration != null)) { // check for default duration + final Object duration = itemConfiguration.get(HomekitValveImpl.CONFIG_DEFAULT_DURATION); if (duration instanceof BigDecimal) { value = ((BigDecimal) duration).intValue(); if (taggedItem.getItem() instanceof NumberItem) { @@ -536,14 +537,14 @@ private static SetDurationCharacteristic createDurationCharacteristic(final Home getUnsubscriber(taggedItem, DURATION, updater)); } - private static RemainingDurationCharacteristic createRemainingDurationCharacteristic( - final HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { + private static RemainingDurationCharacteristic createRemainingDurationCharacteristic(HomekitTaggedItem taggedItem, + HomekitAccessoryUpdater updater) { return new RemainingDurationCharacteristic(getIntSupplier(taggedItem), getSubscriber(taggedItem, REMAINING_DURATION, updater), getUnsubscriber(taggedItem, REMAINING_DURATION, updater)); } - private static VolumeCharacteristic createVolumeCharacteristic(final HomekitTaggedItem taggedItem, + private static VolumeCharacteristic createVolumeCharacteristic(HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new VolumeCharacteristic(getIntSupplier(taggedItem), (volume) -> ((NumberItem) taggedItem.getItem()).send(new DecimalType(volume)), @@ -551,7 +552,7 @@ private static VolumeCharacteristic createVolumeCharacteristic(final HomekitTagg } private static CoolingThresholdTemperatureCharacteristic createCoolingThresholdCharacteristic( - final HomekitTaggedItem taggedItem, final HomekitAccessoryUpdater updater) { + HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new CoolingThresholdTemperatureCharacteristic( taggedItem.getConfigurationAsDouble(HomekitTaggedItem.MIN_VALUE, CoolingThresholdTemperatureCharacteristic.DEFAULT_MIN_VALUE), @@ -565,7 +566,7 @@ private static CoolingThresholdTemperatureCharacteristic createCoolingThresholdC } private static HeatingThresholdTemperatureCharacteristic createHeatingThresholdCharacteristic( - final HomekitTaggedItem taggedItem, final HomekitAccessoryUpdater updater) { + HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { return new HeatingThresholdTemperatureCharacteristic( taggedItem.getConfigurationAsDouble(HomekitTaggedItem.MIN_VALUE, HeatingThresholdTemperatureCharacteristic.DEFAULT_MIN_VALUE), diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitContactSensorImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitContactSensorImpl.java index 229a77caf5a1f..8ed9132225ca3 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitContactSensorImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitContactSensorImpl.java @@ -12,14 +12,14 @@ */ package org.openhab.io.homekit.internal.accessories; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.CONTACT_SENSOR_STATE; + import java.util.List; import java.util.concurrent.CompletableFuture; -import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; -import org.openhab.io.homekit.internal.HomekitCharacteristicType; import org.openhab.io.homekit.internal.HomekitSettings; import org.openhab.io.homekit.internal.HomekitTaggedItem; @@ -38,26 +38,23 @@ public class HomekitContactSensorImpl extends AbstractHomekitAccessoryImpl imple public HomekitContactSensorImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); - this.contactSensedReader = new BooleanItemReader( - getItem(HomekitCharacteristicType.CONTACT_SENSOR_STATE, GenericItem.class), OnOffType.ON, - OpenClosedType.OPEN); + contactSensedReader = createBooleanReader(CONTACT_SENSOR_STATE, OnOffType.ON, OpenClosedType.OPEN); getServices().add(new ContactSensorService(this)); } @Override public CompletableFuture getCurrentState() { - Boolean isOpen = contactSensedReader.getValue(); - return CompletableFuture - .completedFuture((isOpen == Boolean.TRUE) ? ContactStateEnum.NOT_DETECTED : ContactStateEnum.DETECTED); + return CompletableFuture.completedFuture( + contactSensedReader.getValue() ? ContactStateEnum.NOT_DETECTED : ContactStateEnum.DETECTED); } @Override public void subscribeContactState(HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.CONTACT_SENSOR_STATE, callback); + subscribe(CONTACT_SENSOR_STATE, callback); } @Override public void unsubscribeContactState() { - unsubscribe(HomekitCharacteristicType.CONTACT_SENSOR_STATE); + unsubscribe(CONTACT_SENSOR_STATE); } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitFanImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitFanImpl.java index 985806852c217..7da1dc98db859 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitFanImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitFanImpl.java @@ -12,15 +12,14 @@ */ package org.openhab.io.homekit.internal.accessories; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.ACTIVE_STATUS; + import java.util.List; import java.util.concurrent.CompletableFuture; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.smarthome.core.library.items.SwitchItem; import org.eclipse.smarthome.core.library.types.OnOffType; -import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; -import org.openhab.io.homekit.internal.HomekitCharacteristicType; import org.openhab.io.homekit.internal.HomekitSettings; import org.openhab.io.homekit.internal.HomekitTaggedItem; @@ -34,34 +33,33 @@ * @author Eugen Freiter - Initial contribution */ class HomekitFanImpl extends AbstractHomekitAccessoryImpl implements FanAccessory { + private final BooleanItemReader activeReader; + public HomekitFanImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); + activeReader = createBooleanReader(ACTIVE_STATUS, OnOffType.ON, OpenClosedType.OPEN); this.getServices().add(new FanService(this)); } @Override public CompletableFuture isActive() { - final @Nullable State state = getStateAs(HomekitCharacteristicType.ACTIVE_STATUS, OnOffType.class); - return CompletableFuture.completedFuture(state == OnOffType.ON); + return CompletableFuture.completedFuture(activeReader.getValue()); } @Override - public CompletableFuture setActive(final boolean state) throws Exception { - final @Nullable SwitchItem item = getItem(HomekitCharacteristicType.ACTIVE_STATUS, SwitchItem.class); - if (item != null) { - item.send(OnOffType.from(state)); - } + public CompletableFuture setActive(boolean state) { + activeReader.setValue(state); return CompletableFuture.completedFuture(null); } @Override - public void subscribeActive(final HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.ACTIVE_STATUS, callback); + public void subscribeActive(HomekitCharacteristicChangeCallback callback) { + subscribe(ACTIVE_STATUS, callback); } @Override public void unsubscribeActive() { - unsubscribe(HomekitCharacteristicType.ACTIVE_STATUS); + unsubscribe(ACTIVE_STATUS); } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitGarageDoorOpenerImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitGarageDoorOpenerImpl.java index bd3ee7d9d9195..680c2da7e84f8 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitGarageDoorOpenerImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitGarageDoorOpenerImpl.java @@ -12,12 +12,14 @@ */ package org.openhab.io.homekit.internal.accessories; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.CURRENT_DOOR_STATE; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.OBSTRUCTION_STATUS; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.TARGET_DOOR_STATE; + import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.items.Item; import org.eclipse.smarthome.core.library.items.StringItem; import org.eclipse.smarthome.core.library.items.SwitchItem; @@ -25,7 +27,6 @@ import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.eclipse.smarthome.core.library.types.StringType; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; -import org.openhab.io.homekit.internal.HomekitCharacteristicType; import org.openhab.io.homekit.internal.HomekitSettings; import org.openhab.io.homekit.internal.HomekitTaggedItem; import org.slf4j.Logger; @@ -43,24 +44,25 @@ * @author Eugen Freiter - Initial contribution */ public class HomekitGarageDoorOpenerImpl extends AbstractHomekitAccessoryImpl implements GarageDoorOpenerAccessory { - private Logger logger = LoggerFactory.getLogger(HomekitGarageDoorOpenerImpl.class); + private final Logger logger = LoggerFactory.getLogger(HomekitGarageDoorOpenerImpl.class); + private final BooleanItemReader obstructionReader; public HomekitGarageDoorOpenerImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); - this.getServices().add(new GarageDoorOpenerService(this)); + obstructionReader = createBooleanReader(OBSTRUCTION_STATUS, OnOffType.ON, OpenClosedType.OPEN); + getServices().add(new GarageDoorOpenerService(this)); } @Override public CompletableFuture getCurrentDoorState() { - final Optional characteristic = getCharacteristic( - HomekitCharacteristicType.CURRENT_DOOR_STATE); + final Optional characteristic = getCharacteristic(CURRENT_DOOR_STATE); final HomekitSettings settings = getSettings(); String stringValue = settings.doorCurrentStateClosed; if (characteristic.isPresent()) { stringValue = characteristic.get().getItem().getState().toString(); } else { - logger.warn("Missing mandatory characteristic {}", HomekitCharacteristicType.CURRENT_DOOR_STATE); + logger.warn("Missing mandatory characteristic {}", CURRENT_DOOR_STATE); } CurrentDoorStateEnum mode; @@ -75,7 +77,7 @@ public CompletableFuture getCurrentDoorState() { } else if (stringValue.equalsIgnoreCase(settings.doorCurrentStateStopped)) { mode = CurrentDoorStateEnum.SOPPED; } else if (stringValue.equals("UNDEF") || stringValue.equals("NULL")) { - logger.warn("Current door state not available. Relaying value of CLOSED to Homekit"); + logger.warn("Current door state not available. Relaying value of CLOSED to HomeKit"); mode = CurrentDoorStateEnum.CLOSED; } else { logger.warn("Unrecognized current door state: {}. Expected {}, {}, {}, {} or {} strings in value.", @@ -88,14 +90,13 @@ public CompletableFuture getCurrentDoorState() { @Override public CompletableFuture getTargetDoorState() { - final Optional characteristic = getCharacteristic( - HomekitCharacteristicType.TARGET_DOOR_STATE); + final Optional characteristic = getCharacteristic(TARGET_DOOR_STATE); Item item; if (characteristic.isPresent()) { item = characteristic.get().getItem(); } else { - logger.warn("Missing mandatory characteristic {}", HomekitCharacteristicType.TARGET_DOOR_STATE); + logger.warn("Missing mandatory characteristic {}", TARGET_DOOR_STATE); return CompletableFuture.completedFuture(TargetDoorStateEnum.CLOSED); } TargetDoorStateEnum mode; @@ -109,7 +110,6 @@ public CompletableFuture getTargetDoorState() { mode = TargetDoorStateEnum.CLOSED; } else if (stringValue.equalsIgnoreCase(settings.doorTargetStateOpen)) { mode = TargetDoorStateEnum.OPEN; - ; } else { logger.warn( "Unsupported value {} for {}. Only {} and {} supported. Check HomeKit settings if you want to change the mapping", @@ -126,23 +126,17 @@ public CompletableFuture getTargetDoorState() { @Override public CompletableFuture getObstructionDetected() { - final @Nullable Item item = getItem(HomekitCharacteristicType.OBSTRUCTION_STATUS, GenericItem.class); - if (item == null) { - logger.warn("Missing mandatory characteristic {}", HomekitCharacteristicType.OBSTRUCTION_STATUS); - } - return CompletableFuture - .completedFuture(item.getState() == OnOffType.ON || item.getState() == OpenClosedType.OPEN); + return CompletableFuture.completedFuture(obstructionReader.getValue()); } @Override - public CompletableFuture setTargetDoorState(final TargetDoorStateEnum targetDoorStateEnum) { - final Optional characteristic = getCharacteristic( - HomekitCharacteristicType.TARGET_DOOR_STATE); + public CompletableFuture setTargetDoorState(TargetDoorStateEnum targetDoorStateEnum) { + final Optional characteristic = getCharacteristic(TARGET_DOOR_STATE); Item item; if (characteristic.isPresent()) { item = characteristic.get().getItem(); } else { - logger.warn("Missing mandatory characteristic {}", HomekitCharacteristicType.TARGET_DOOR_STATE); + logger.warn("Missing mandatory characteristic {}", TARGET_DOOR_STATE); return CompletableFuture.completedFuture(null); } @@ -161,32 +155,32 @@ public CompletableFuture setTargetDoorState(final TargetDoorStateEnum targ } @Override - public void subscribeCurrentDoorState(final HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.CURRENT_DOOR_STATE, callback); + public void subscribeCurrentDoorState(HomekitCharacteristicChangeCallback callback) { + subscribe(CURRENT_DOOR_STATE, callback); } @Override - public void subscribeTargetDoorState(final HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.TARGET_DOOR_STATE, callback); + public void subscribeTargetDoorState(HomekitCharacteristicChangeCallback callback) { + subscribe(TARGET_DOOR_STATE, callback); } @Override - public void subscribeObstructionDetected(final HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.OBSTRUCTION_STATUS, callback); + public void subscribeObstructionDetected(HomekitCharacteristicChangeCallback callback) { + subscribe(OBSTRUCTION_STATUS, callback); } @Override public void unsubscribeCurrentDoorState() { - unsubscribe(HomekitCharacteristicType.CURRENT_DOOR_STATE); + unsubscribe(CURRENT_DOOR_STATE); } @Override public void unsubscribeTargetDoorState() { - unsubscribe(HomekitCharacteristicType.TARGET_DOOR_STATE); + unsubscribe(TARGET_DOOR_STATE); } @Override public void unsubscribeObstructionDetected() { - unsubscribe(HomekitCharacteristicType.OBSTRUCTION_STATUS); + unsubscribe(OBSTRUCTION_STATUS); } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHeaterCoolerImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHeaterCoolerImpl.java index 8d67664548ecd..ca315bf018a82 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHeaterCoolerImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHeaterCoolerImpl.java @@ -12,6 +12,7 @@ */ package org.openhab.io.homekit.internal.accessories; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.ACTIVE_STATUS; import static org.openhab.io.homekit.internal.HomekitCharacteristicType.CURRENT_HEATER_COOLER_STATE; import static org.openhab.io.homekit.internal.HomekitCharacteristicType.TARGET_HEATER_COOLER_STATE; @@ -24,12 +25,10 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.library.items.StringItem; -import org.eclipse.smarthome.core.library.items.SwitchItem; import org.eclipse.smarthome.core.library.types.DecimalType; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.eclipse.smarthome.core.library.types.StringType; -import org.eclipse.smarthome.core.types.State; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; import org.openhab.io.homekit.internal.HomekitCharacteristicType; import org.openhab.io.homekit.internal.HomekitSettings; @@ -54,17 +53,16 @@ public class HomekitHeaterCoolerImpl extends AbstractHomekitAccessoryImpl implements HeaterCoolerAccessory { private final Logger logger = LoggerFactory.getLogger(HomekitHeaterCoolerImpl.class); private final BooleanItemReader activeReader; - private final Map currentStateMapping = new EnumMap( + private final Map currentStateMapping = new EnumMap( CurrentHeaterCoolerStateEnum.class) { { put(CurrentHeaterCoolerStateEnum.INACTIVE, "INACTIVE"); put(CurrentHeaterCoolerStateEnum.IDLE, "IDLE"); put(CurrentHeaterCoolerStateEnum.HEATING, "HEATING"); put(CurrentHeaterCoolerStateEnum.COOLING, "COOLING"); - } }; - private final Map targetStateMapping = new EnumMap( + private final Map targetStateMapping = new EnumMap( TargetHeaterCoolerStateEnum.class) { { put(TargetHeaterCoolerStateEnum.AUTO, "AUTO"); @@ -76,36 +74,32 @@ public class HomekitHeaterCoolerImpl extends AbstractHomekitAccessoryImpl implem public HomekitHeaterCoolerImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); - activeReader = new BooleanItemReader(getItem(HomekitCharacteristicType.ACTIVE_STATUS, GenericItem.class), - OnOffType.ON, OpenClosedType.OPEN); + activeReader = new BooleanItemReader(getItem(ACTIVE_STATUS, GenericItem.class) + .orElseThrow(() -> new IncompleteAccessoryException(ACTIVE_STATUS)), OnOffType.ON, OpenClosedType.OPEN); updateMapping(CURRENT_HEATER_COOLER_STATE, currentStateMapping); updateMapping(TARGET_HEATER_COOLER_STATE, targetStateMapping); final HeaterCoolerService service = new HeaterCoolerService(this); - service.addOptionalCharacteristic(new TemperatureDisplayUnitCharacteristic(() -> getTemperatureDisplayUnit(), - (value) -> setTemperatureDisplayUnit(value), (callback) -> subscribeTemperatureDisplayUnit(callback), - () -> unsubscribeTemperatureDisplayUnit())); + service.addOptionalCharacteristic(new TemperatureDisplayUnitCharacteristic(this::getTemperatureDisplayUnit, + this::setTemperatureDisplayUnit, this::subscribeTemperatureDisplayUnit, + this::unsubscribeTemperatureDisplayUnit)); getServices().add(service); } @Override public CompletableFuture getCurrentTemperature() { - @Nullable - final DecimalType state = getStateAs(HomekitCharacteristicType.CURRENT_TEMPERATURE, DecimalType.class); + final @Nullable DecimalType state = getStateAs(HomekitCharacteristicType.CURRENT_TEMPERATURE, + DecimalType.class); return CompletableFuture.completedFuture(state != null ? convertToCelsius(state.doubleValue()) : 0.0); } @Override public CompletableFuture isActive() { - final @Nullable State state = getStateAs(HomekitCharacteristicType.ACTIVE_STATUS, OnOffType.class); - return CompletableFuture.completedFuture(state == OnOffType.ON); + return CompletableFuture.completedFuture(activeReader.getValue()); } @Override - public CompletableFuture setActive(final boolean state) { - final @Nullable SwitchItem item = getItem(HomekitCharacteristicType.ACTIVE_STATUS, SwitchItem.class); - if (item != null) { - item.send(OnOffType.from(state)); - } + public CompletableFuture setActive(boolean state) { + activeReader.setValue(state); return CompletableFuture.completedFuture(null); } @@ -122,7 +116,7 @@ public CompletableFuture getTargetHeaterCoolerState } @Override - public CompletableFuture setTargetHeaterCoolerState(final TargetHeaterCoolerStateEnum state) { + public CompletableFuture setTargetHeaterCoolerState(TargetHeaterCoolerStateEnum state) { final Optional characteristic = getCharacteristic( HomekitCharacteristicType.TARGET_HEATER_COOLER_STATE); if (characteristic.isPresent()) { @@ -140,13 +134,13 @@ public CompletableFuture getTemperatureDisplayUnit() : TemperatureDisplayUnitEnum.CELSIUS); } - public void setTemperatureDisplayUnit(final TemperatureDisplayUnitEnum value) throws Exception { + public void setTemperatureDisplayUnit(TemperatureDisplayUnitEnum value) { // temperature unit set globally via binding setting and cannot be changed at item level. // this method is intentionally empty. } @Override - public void subscribeCurrentHeaterCoolerState(final HomekitCharacteristicChangeCallback callback) { + public void subscribeCurrentHeaterCoolerState(HomekitCharacteristicChangeCallback callback) { subscribe(HomekitCharacteristicType.CURRENT_HEATER_COOLER_STATE, callback); } @@ -156,7 +150,7 @@ public void unsubscribeCurrentHeaterCoolerState() { } @Override - public void subscribeTargetHeaterCoolerState(final HomekitCharacteristicChangeCallback callback) { + public void subscribeTargetHeaterCoolerState(HomekitCharacteristicChangeCallback callback) { subscribe(HomekitCharacteristicType.TARGET_HEATER_COOLER_STATE, callback); } @@ -166,17 +160,17 @@ public void unsubscribeTargetHeaterCoolerState() { } @Override - public void subscribeActive(final HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.ACTIVE_STATUS, callback); + public void subscribeActive(HomekitCharacteristicChangeCallback callback) { + subscribe(ACTIVE_STATUS, callback); } @Override public void unsubscribeActive() { - unsubscribe(HomekitCharacteristicType.ACTIVE_STATUS); + unsubscribe(ACTIVE_STATUS); } @Override - public void subscribeCurrentTemperature(final HomekitCharacteristicChangeCallback callback) { + public void subscribeCurrentTemperature(HomekitCharacteristicChangeCallback callback) { subscribe(HomekitCharacteristicType.CURRENT_TEMPERATURE, callback); } @@ -185,7 +179,7 @@ public void unsubscribeCurrentTemperature() { unsubscribe(HomekitCharacteristicType.CURRENT_TEMPERATURE); } - public void subscribeTemperatureDisplayUnit(final HomekitCharacteristicChangeCallback callback) { + public void subscribeTemperatureDisplayUnit(HomekitCharacteristicChangeCallback callback) { // temperature unit set globally via binding setting and cannot be changed at item level. // this method is intentionally empty } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHumiditySensorImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHumiditySensorImpl.java index e7b1358788156..16e9fbc71a948 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHumiditySensorImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHumiditySensorImpl.java @@ -35,15 +35,14 @@ public class HomekitHumiditySensorImpl extends AbstractHomekitAccessoryImpl impl private final static String CONFIG_MULTIPLICATOR = "homekitMultiplicator"; public HomekitHumiditySensorImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, - HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { + HomekitAccessoryUpdater updater, HomekitSettings settings) { super(taggedItem, mandatoryCharacteristics, updater, settings); getServices().add(new HumiditySensorService(this)); } @Override public CompletableFuture getCurrentRelativeHumidity() { - @Nullable - DecimalType state = getStateAs(HomekitCharacteristicType.RELATIVE_HUMIDITY, DecimalType.class); + final @Nullable DecimalType state = getStateAs(HomekitCharacteristicType.RELATIVE_HUMIDITY, DecimalType.class); if (state != null) { BigDecimal multiplicator = getAccessoryConfiguration(CONFIG_MULTIPLICATOR, BigDecimal.valueOf(1.0)); return CompletableFuture.completedFuture((state.toBigDecimal().multiply(multiplicator)).doubleValue()); diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLeakSensorImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLeakSensorImpl.java index 87da6d8aa2b12..33febd3d412dd 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLeakSensorImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLeakSensorImpl.java @@ -12,14 +12,14 @@ */ package org.openhab.io.homekit.internal.accessories; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.LEAK_DETECTED_STATE; + import java.util.List; import java.util.concurrent.CompletableFuture; -import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; -import org.openhab.io.homekit.internal.HomekitCharacteristicType; import org.openhab.io.homekit.internal.HomekitSettings; import org.openhab.io.homekit.internal.HomekitTaggedItem; @@ -38,27 +38,23 @@ public class HomekitLeakSensorImpl extends AbstractHomekitAccessoryImpl implemen public HomekitLeakSensorImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); - this.leakDetectedReader = new BooleanItemReader( - getItem(HomekitCharacteristicType.LEAK_DETECTED_STATE, GenericItem.class), OnOffType.ON, - OpenClosedType.OPEN); + leakDetectedReader = createBooleanReader(LEAK_DETECTED_STATE, OnOffType.ON, OpenClosedType.OPEN); getServices().add(new LeakSensorService(this)); } @Override public CompletableFuture getLeakDetected() { - return CompletableFuture - .completedFuture((this.leakDetectedReader.getValue() != null && this.leakDetectedReader.getValue()) - ? LeakDetectedStateEnum.LEAK_DETECTED - : LeakDetectedStateEnum.LEAK_NOT_DETECTED); + return CompletableFuture.completedFuture(leakDetectedReader.getValue() ? LeakDetectedStateEnum.LEAK_DETECTED + : LeakDetectedStateEnum.LEAK_NOT_DETECTED); } @Override public void subscribeLeakDetected(HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.LEAK_DETECTED_STATE, callback); + subscribe(LEAK_DETECTED_STATE, callback); } @Override public void unsubscribeLeakDetected() { - unsubscribe(HomekitCharacteristicType.LEAK_DETECTED_STATE); + unsubscribe(LEAK_DETECTED_STATE); } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLightbulbImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLightbulbImpl.java index 5bd1782ee5ad5..81c1f11a5b81c 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLightbulbImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLightbulbImpl.java @@ -38,7 +38,7 @@ class HomekitLightbulbImpl extends AbstractHomekitAccessoryImpl implements LightbulbAccessory { public HomekitLightbulbImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, - HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { + HomekitAccessoryUpdater updater, HomekitSettings settings) { super(taggedItem, mandatoryCharacteristics, updater, settings); this.getServices().add(new LightbulbService(this)); } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLockImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLockImpl.java index b4645c84989d1..da4938b4b7057 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLockImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitLockImpl.java @@ -13,11 +13,11 @@ package org.openhab.io.homekit.internal.accessories; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.items.GenericItem; -import org.eclipse.smarthome.core.items.Item; import org.eclipse.smarthome.core.library.items.SwitchItem; import org.eclipse.smarthome.core.library.types.DecimalType; import org.eclipse.smarthome.core.library.types.OnOffType; @@ -42,65 +42,63 @@ public class HomekitLockImpl extends AbstractHomekitAccessoryImpl implements LockMechanismAccessory { public HomekitLockImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, - HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { + HomekitAccessoryUpdater updater, HomekitSettings settings) { super(taggedItem, mandatoryCharacteristics, updater, settings); getServices().add(new LockMechanismService(this)); } @Override public CompletableFuture getLockCurrentState() { - @Nullable - - final State state = getItem(HomekitCharacteristicType.LOCK_CURRENT_STATE, GenericItem.class).getState(); - if (state instanceof DecimalType) { - return CompletableFuture.completedFuture(LockCurrentStateEnum.fromCode( - state.as(DecimalType.class).intValue())); - } else if (state instanceof OnOffType) { - return CompletableFuture.completedFuture( - state == OnOffType.ON ? LockCurrentStateEnum.SECURED : LockCurrentStateEnum.UNSECURED); + final Optional item = getItem(HomekitCharacteristicType.LOCK_CURRENT_STATE, GenericItem.class); + LockCurrentStateEnum lockState = LockCurrentStateEnum.UNKNOWN; + if (item.isPresent()) { + final State state = item.get().getState(); + if (state instanceof DecimalType) { + lockState = LockCurrentStateEnum.fromCode(((DecimalType) state).intValue()); + } else if (state instanceof OnOffType) { + lockState = state == OnOffType.ON ? LockCurrentStateEnum.SECURED : LockCurrentStateEnum.UNSECURED; + } } - return CompletableFuture.completedFuture(LockCurrentStateEnum.UNKNOWN); + return CompletableFuture.completedFuture(lockState); } @Override public CompletableFuture getLockTargetState() { - @Nullable - OnOffType state = getStateAs(HomekitCharacteristicType.LOCK_TARGET_STATE, OnOffType.class); + final @Nullable OnOffType state = getStateAs(HomekitCharacteristicType.LOCK_TARGET_STATE, OnOffType.class); if (state != null) { return CompletableFuture.completedFuture( state == OnOffType.ON ? LockTargetStateEnum.SECURED : LockTargetStateEnum.UNSECURED); } return CompletableFuture.completedFuture(LockTargetStateEnum.UNSECURED); - // Apple HAP specification has onyl SECURED and UNSECURED values for lock target state. + // Apple HAP specification has only SECURED and UNSECURED values for lock target state. // unknown does not supported for target state. } @Override - public CompletableFuture setLockTargetState(final LockTargetStateEnum state) { - @Nullable - Item item = getItem(HomekitCharacteristicType.LOCK_TARGET_STATE, SwitchItem.class); - if (item != null) + public CompletableFuture setLockTargetState(LockTargetStateEnum state) { + getItem(HomekitCharacteristicType.LOCK_TARGET_STATE, SwitchItem.class).ifPresent(item -> { switch (state) { case SECURED: // Close the door if (item instanceof SwitchItem) { - ((SwitchItem) item).send(OnOffType.ON); + item.send(OnOffType.ON); } break; case UNSECURED: // Open the door if (item instanceof SwitchItem) { - ((SwitchItem) item).send(OnOffType.OFF); + item.send(OnOffType.OFF); } break; default: break; } + }); return CompletableFuture.completedFuture(null); } @Override - public void subscribeLockCurrentState(final HomekitCharacteristicChangeCallback callback) { + public void subscribeLockCurrentState(HomekitCharacteristicChangeCallback callback) { subscribe(HomekitCharacteristicType.LOCK_CURRENT_STATE, callback); } @@ -110,7 +108,7 @@ public void unsubscribeLockCurrentState() { } @Override - public void subscribeLockTargetState(final HomekitCharacteristicChangeCallback callback) { + public void subscribeLockTargetState(HomekitCharacteristicChangeCallback callback) { subscribe(HomekitCharacteristicType.LOCK_TARGET_STATE, callback); } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitMotionSensorImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitMotionSensorImpl.java index 1aaa7b599bb7e..b408879bdcafd 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitMotionSensorImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitMotionSensorImpl.java @@ -15,7 +15,6 @@ import java.util.List; import java.util.concurrent.CompletableFuture; -import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; @@ -37,17 +36,14 @@ public class HomekitMotionSensorImpl extends AbstractHomekitAccessoryImpl implem public HomekitMotionSensorImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); - this.motionSensedReader = new BooleanItemReader( - getItem(HomekitCharacteristicType.MOTION_DETECTED_STATE, GenericItem.class), OnOffType.ON, + motionSensedReader = createBooleanReader(HomekitCharacteristicType.MOTION_DETECTED_STATE, OnOffType.ON, OpenClosedType.OPEN); - getServices().add(new MotionSensorService(this)); } @Override public CompletableFuture getMotionDetected() { - return CompletableFuture - .completedFuture((this.motionSensedReader.getValue() != null) ? motionSensedReader.getValue() : false); + return CompletableFuture.completedFuture(motionSensedReader.getValue()); } @Override diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitOccupancySensorImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitOccupancySensorImpl.java index b2b6761c4c496..8966ceff0a45a 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitOccupancySensorImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitOccupancySensorImpl.java @@ -12,14 +12,14 @@ */ package org.openhab.io.homekit.internal.accessories; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.OCCUPANCY_DETECTED_STATE; + import java.util.List; import java.util.concurrent.CompletableFuture; -import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; -import org.openhab.io.homekit.internal.HomekitCharacteristicType; import org.openhab.io.homekit.internal.HomekitSettings; import org.openhab.io.homekit.internal.HomekitTaggedItem; @@ -38,26 +38,23 @@ public class HomekitOccupancySensorImpl extends AbstractHomekitAccessoryImpl imp public HomekitOccupancySensorImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); - this.occupancySensedReader = new BooleanItemReader( - getItem(HomekitCharacteristicType.OCCUPANCY_DETECTED_STATE, GenericItem.class), OnOffType.ON, - OpenClosedType.OPEN); + occupancySensedReader = createBooleanReader(OCCUPANCY_DETECTED_STATE, OnOffType.ON, OpenClosedType.OPEN); getServices().add(new OccupancySensorService(this)); } @Override public CompletableFuture getOccupancyDetected() { - return (this.occupancySensedReader.getValue() != null && this.occupancySensedReader.getValue()) - ? CompletableFuture.completedFuture(OccupancyDetectedEnum.DETECTED) - : CompletableFuture.completedFuture(OccupancyDetectedEnum.NOT_DETECTED); + return CompletableFuture.completedFuture( + occupancySensedReader.getValue() ? OccupancyDetectedEnum.DETECTED : OccupancyDetectedEnum.NOT_DETECTED); } @Override public void subscribeOccupancyDetected(HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.OCCUPANCY_DETECTED_STATE, callback); + subscribe(OCCUPANCY_DETECTED_STATE, callback); } @Override public void unsubscribeOccupancyDetected() { - unsubscribe(HomekitCharacteristicType.OCCUPANCY_DETECTED_STATE); + unsubscribe(OCCUPANCY_DETECTED_STATE); } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitOutletImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitOutletImpl.java index d73cb702c484e..9c6f338280c48 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitOutletImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitOutletImpl.java @@ -15,15 +15,12 @@ import java.util.List; import java.util.concurrent.CompletableFuture; -import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; import org.openhab.io.homekit.internal.HomekitCharacteristicType; import org.openhab.io.homekit.internal.HomekitSettings; import org.openhab.io.homekit.internal.HomekitTaggedItem; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import io.github.hapjava.accessories.OutletAccessory; import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; @@ -34,44 +31,40 @@ * @author Eugen Freiter - Initial contribution */ public class HomekitOutletImpl extends AbstractHomekitAccessoryImpl implements OutletAccessory { - private final Logger logger = LoggerFactory.getLogger(HomekitOutletImpl.class); - private final BooleanItemReader inUseReader; private final BooleanItemReader onReader; public HomekitOutletImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); - this.inUseReader = new BooleanItemReader(getItem(HomekitCharacteristicType.INUSE_STATUS, GenericItem.class), - OnOffType.ON, OpenClosedType.OPEN); - this.onReader = new BooleanItemReader(getItem(HomekitCharacteristicType.ON_STATE, GenericItem.class), - OnOffType.ON, OpenClosedType.OPEN); + inUseReader = createBooleanReader(HomekitCharacteristicType.INUSE_STATUS, OnOffType.ON, OpenClosedType.OPEN); + onReader = createBooleanReader(HomekitCharacteristicType.ON_STATE, OnOffType.ON, OpenClosedType.OPEN); getServices().add(new OutletService(this)); } @Override public CompletableFuture getPowerState() { - return CompletableFuture.completedFuture(this.onReader.getValue() != null && this.onReader.getValue()); + return CompletableFuture.completedFuture(onReader.getValue()); } @Override public CompletableFuture getOutletInUse() { - return CompletableFuture.completedFuture(this.inUseReader.getValue() != null && this.onReader.getValue()); + return CompletableFuture.completedFuture(inUseReader.getValue()); } @Override - public CompletableFuture setPowerState(final boolean state) throws Exception { - this.onReader.setValue(state); + public CompletableFuture setPowerState(boolean state) { + onReader.setValue(state); return CompletableFuture.completedFuture(null); } @Override - public void subscribePowerState(final HomekitCharacteristicChangeCallback callback) { + public void subscribePowerState(HomekitCharacteristicChangeCallback callback) { subscribe(HomekitCharacteristicType.ON_STATE, callback); } @Override - public void subscribeOutletInUse(final HomekitCharacteristicChangeCallback callback) { + public void subscribeOutletInUse(HomekitCharacteristicChangeCallback callback) { subscribe(HomekitCharacteristicType.INUSE_STATUS, callback); } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSecuritySystemImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSecuritySystemImpl.java index 766cda2fa7027..53e3c023d5a77 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSecuritySystemImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSecuritySystemImpl.java @@ -18,7 +18,6 @@ import java.util.EnumMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.CompletableFuture; import org.eclipse.smarthome.core.library.items.StringItem; @@ -27,8 +26,6 @@ import org.openhab.io.homekit.internal.HomekitCharacteristicType; import org.openhab.io.homekit.internal.HomekitSettings; import org.openhab.io.homekit.internal.HomekitTaggedItem; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import io.github.hapjava.accessories.SecuritySystemAccessory; import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; @@ -46,7 +43,6 @@ * @author Cody Cutrer - Initial contribution */ public class HomekitSecuritySystemImpl extends AbstractHomekitAccessoryImpl implements SecuritySystemAccessory { - private final Logger logger = LoggerFactory.getLogger(HomekitSecuritySystemImpl.class); private final Map currentStateMapping = new EnumMap( CurrentSecuritySystemStateEnum.class) { { @@ -68,7 +64,7 @@ public class HomekitSecuritySystemImpl extends AbstractHomekitAccessoryImpl impl }; public HomekitSecuritySystemImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, - HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { + HomekitAccessoryUpdater updater, HomekitSettings settings) { super(taggedItem, mandatoryCharacteristics, updater, settings); updateMapping(SECURITY_SYSTEM_CURRENT_STATE, currentStateMapping); updateMapping(SECURITY_SYSTEM_TARGET_STATE, targetStateMapping); @@ -82,15 +78,9 @@ public CompletableFuture getCurrentSecuritySyste } @Override - public void setTargetSecuritySystemState(final TargetSecuritySystemStateEnum state) { - final Optional characteristic = getCharacteristic( - HomekitCharacteristicType.SECURITY_SYSTEM_TARGET_STATE); - if (characteristic.isPresent()) { - ((StringItem) characteristic.get().getItem()).send(new StringType(targetStateMapping.get(state))); - } else { - logger.warn("Missing mandatory characteristic {}", - HomekitCharacteristicType.SECURITY_SYSTEM_TARGET_STATE.getTag()); - } + public void setTargetSecuritySystemState(TargetSecuritySystemStateEnum state) { + getItem(HomekitCharacteristicType.SECURITY_SYSTEM_TARGET_STATE, StringItem.class) + .ifPresent(item -> item.send(new StringType(targetStateMapping.get(state)))); } @Override @@ -100,7 +90,7 @@ public CompletableFuture getTargetSecuritySystemS } @Override - public void subscribeCurrentSecuritySystemState(final HomekitCharacteristicChangeCallback callback) { + public void subscribeCurrentSecuritySystemState(HomekitCharacteristicChangeCallback callback) { subscribe(SECURITY_SYSTEM_CURRENT_STATE, callback); } @@ -110,7 +100,7 @@ public void unsubscribeCurrentSecuritySystemState() { } @Override - public void subscribeTargetSecuritySystemState(final HomekitCharacteristicChangeCallback callback) { + public void subscribeTargetSecuritySystemState(HomekitCharacteristicChangeCallback callback) { subscribe(SECURITY_SYSTEM_TARGET_STATE, callback); } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSmokeSensorImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSmokeSensorImpl.java index 51905acaa2d7d..a0a880ce347dd 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSmokeSensorImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSmokeSensorImpl.java @@ -12,14 +12,14 @@ */ package org.openhab.io.homekit.internal.accessories; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.SMOKE_DETECTED_STATE; + import java.util.List; import java.util.concurrent.CompletableFuture; -import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; -import org.openhab.io.homekit.internal.HomekitCharacteristicType; import org.openhab.io.homekit.internal.HomekitSettings; import org.openhab.io.homekit.internal.HomekitTaggedItem; @@ -33,32 +33,28 @@ * @author Cody Cutrer - Initial contribution */ public class HomekitSmokeSensorImpl extends AbstractHomekitAccessoryImpl implements SmokeSensorAccessory { - private final BooleanItemReader smokeDetectedReader; public HomekitSmokeSensorImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); - this.smokeDetectedReader = new BooleanItemReader( - getItem(HomekitCharacteristicType.SMOKE_DETECTED_STATE, GenericItem.class), OnOffType.ON, - OpenClosedType.OPEN); + smokeDetectedReader = createBooleanReader(SMOKE_DETECTED_STATE, OnOffType.ON, OpenClosedType.OPEN); this.getServices().add(new SmokeSensorService(this)); } @Override public CompletableFuture getSmokeDetectedState() { - Boolean state = this.smokeDetectedReader.getValue(); return CompletableFuture.completedFuture( - (state != null && state) ? SmokeDetectedStateEnum.DETECTED : SmokeDetectedStateEnum.NOT_DETECTED); + smokeDetectedReader.getValue() ? SmokeDetectedStateEnum.DETECTED : SmokeDetectedStateEnum.NOT_DETECTED); } @Override public void subscribeSmokeDetectedState(HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.SMOKE_DETECTED_STATE, callback); + subscribe(SMOKE_DETECTED_STATE, callback); } @Override public void unsubscribeSmokeDetectedState() { - unsubscribe(HomekitCharacteristicType.SMOKE_DETECTED_STATE); + unsubscribe(SMOKE_DETECTED_STATE); } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSpeakerImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSpeakerImpl.java index 8383c9a6129a7..0f4c4027f80d1 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSpeakerImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSpeakerImpl.java @@ -15,7 +15,6 @@ import java.util.List; import java.util.concurrent.CompletableFuture; -import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; @@ -38,24 +37,23 @@ public class HomekitSpeakerImpl extends AbstractHomekitAccessoryImpl implements public HomekitSpeakerImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); - this.muteReader = new BooleanItemReader(getItem(HomekitCharacteristicType.MUTE, GenericItem.class), - OnOffType.ON, OpenClosedType.OPEN); + muteReader = createBooleanReader(HomekitCharacteristicType.MUTE, OnOffType.ON, OpenClosedType.OPEN); getServices().add(new SpeakerService(this)); } @Override public CompletableFuture isMuted() { - return CompletableFuture.completedFuture(this.muteReader.getValue() != null && this.muteReader.getValue()); + return CompletableFuture.completedFuture(muteReader.getValue()); } @Override - public CompletableFuture setMute(final boolean state) { - this.muteReader.setValue(state); + public CompletableFuture setMute(boolean state) { + muteReader.setValue(state); return CompletableFuture.completedFuture(null); } @Override - public void subscribeMuteState(final HomekitCharacteristicChangeCallback callback) { + public void subscribeMuteState(HomekitCharacteristicChangeCallback callback) { subscribe(HomekitCharacteristicType.MUTE, callback); } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSwitchImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSwitchImpl.java index d1dd579626bb4..acac6e8c4f117 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSwitchImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSwitchImpl.java @@ -12,15 +12,14 @@ */ package org.openhab.io.homekit.internal.accessories; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.ON_STATE; + import java.util.List; import java.util.concurrent.CompletableFuture; -import org.eclipse.smarthome.core.items.GenericItem; -import org.eclipse.smarthome.core.items.GroupItem; -import org.eclipse.smarthome.core.library.items.SwitchItem; import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; -import org.openhab.io.homekit.internal.HomekitCharacteristicType; import org.openhab.io.homekit.internal.HomekitSettings; import org.openhab.io.homekit.internal.HomekitTaggedItem; @@ -34,37 +33,33 @@ * @author Andy Lintner - Initial contribution */ public class HomekitSwitchImpl extends AbstractHomekitAccessoryImpl implements SwitchAccessory { + private final BooleanItemReader onReader; public HomekitSwitchImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); + onReader = createBooleanReader(ON_STATE, OnOffType.ON, OpenClosedType.OPEN); getServices().add(new SwitchService(this)); } @Override public CompletableFuture getSwitchState() { - OnOffType state = getStateAs(HomekitCharacteristicType.ON_STATE, OnOffType.class); - return CompletableFuture.completedFuture(state == OnOffType.ON); + return CompletableFuture.completedFuture(onReader.getValue()); } @Override public CompletableFuture setSwitchState(boolean state) { - GenericItem item = getItem(HomekitCharacteristicType.ON_STATE, GenericItem.class); - if (item instanceof SwitchItem) { - ((SwitchItem) item).send(OnOffType.from(state)); - } else if (item instanceof GroupItem) { - ((GroupItem) item).send(OnOffType.from(state)); - } + onReader.setValue(state); return CompletableFuture.completedFuture(null); } @Override public void subscribeSwitchState(HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.ON_STATE, callback); + subscribe(ON_STATE, callback); } @Override public void unsubscribeSwitchState() { - unsubscribe(HomekitCharacteristicType.ON_STATE); + unsubscribe(ON_STATE); } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitTemperatureSensorImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitTemperatureSensorImpl.java index c7f459c09989f..fdf8af02e4988 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitTemperatureSensorImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitTemperatureSensorImpl.java @@ -36,15 +36,15 @@ class HomekitTemperatureSensorImpl extends AbstractHomekitAccessoryImpl implements TemperatureSensorAccessory { public HomekitTemperatureSensorImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, - HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { + HomekitAccessoryUpdater updater, HomekitSettings settings) { super(taggedItem, mandatoryCharacteristics, updater, settings); getServices().add(new TemperatureSensorService(this)); } @Override public CompletableFuture getCurrentTemperature() { - @Nullable - DecimalType state = getStateAs(HomekitCharacteristicType.CURRENT_TEMPERATURE, DecimalType.class); + final @Nullable DecimalType state = getStateAs(HomekitCharacteristicType.CURRENT_TEMPERATURE, + DecimalType.class); return CompletableFuture.completedFuture(state != null ? convertToCelsius(state.doubleValue()) : 0.0); } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitThermostatImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitThermostatImpl.java index e681e895de225..caa4bef3e0ab7 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitThermostatImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitThermostatImpl.java @@ -48,10 +48,10 @@ * @author Andy Lintner - Initial contribution */ class HomekitThermostatImpl extends AbstractHomekitAccessoryImpl implements ThermostatAccessory { - private Logger logger = LoggerFactory.getLogger(HomekitThermostatImpl.class); + private final Logger logger = LoggerFactory.getLogger(HomekitThermostatImpl.class); public HomekitThermostatImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, - HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { + HomekitAccessoryUpdater updater, HomekitSettings settings) { super(taggedItem, mandatoryCharacteristics, updater, settings); this.getServices().add(new ThermostatService(this)); } @@ -154,7 +154,7 @@ public CompletableFuture getTemperatureDisplayUnit() } @Override - public void setTemperatureDisplayUnit(final TemperatureDisplayUnitEnum value) throws Exception { + public void setTemperatureDisplayUnit(TemperatureDisplayUnitEnum value) { // TODO: add support for display unit change } @@ -245,7 +245,7 @@ public void subscribeTargetTemperature(HomekitCharacteristicChangeCallback callb } @Override - public void subscribeTemperatureDisplayUnit(final HomekitCharacteristicChangeCallback callback) { + public void subscribeTemperatureDisplayUnit(HomekitCharacteristicChangeCallback callback) { // TODO: add support for display unit change } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitValveImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitValveImpl.java index 4433778241072..d661a0fc730ad 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitValveImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitValveImpl.java @@ -12,6 +12,8 @@ */ package org.openhab.io.homekit.internal.accessories; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.ACTIVE_STATUS; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.INUSE_STATUS; import static org.openhab.io.homekit.internal.HomekitCharacteristicType.REMAINING_DURATION; import java.util.HashMap; @@ -73,10 +75,8 @@ public class HomekitValveImpl extends AbstractHomekitAccessoryImpl implements Va public HomekitValveImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); - this.inUseReader = new BooleanItemReader(getItem(HomekitCharacteristicType.INUSE_STATUS, GenericItem.class), - OnOffType.ON, OpenClosedType.OPEN); - this.activeReader = new BooleanItemReader(getItem(HomekitCharacteristicType.ACTIVE_STATUS, GenericItem.class), - OnOffType.ON, OpenClosedType.OPEN); + inUseReader = createBooleanReader(INUSE_STATUS, OnOffType.ON, OpenClosedType.OPEN); + activeReader = createBooleanReader(ACTIVE_STATUS, OnOffType.ON, OpenClosedType.OPEN); ValveService service = new ValveService(this); getServices().add(service); final String timerConfig = getAccessoryConfiguration(CONFIG_TIMER, ""); @@ -141,15 +141,13 @@ private void stopTimer() { @Override public CompletableFuture getValveActive() { - return CompletableFuture.completedFuture( - (this.activeReader.getValue() != null && this.activeReader.getValue()) ? ActiveEnum.ACTIVE - : ActiveEnum.INACTIVE); + return CompletableFuture + .completedFuture(this.activeReader.getValue() ? ActiveEnum.ACTIVE : ActiveEnum.INACTIVE); } @Override public CompletableFuture setValveActive(ActiveEnum state) { - SwitchItem item = getItem(HomekitCharacteristicType.ACTIVE_STATUS, SwitchItem.class); - if (item != null) { + getItem(ACTIVE_STATUS, SwitchItem.class).ifPresent(item -> { item.send(OnOffType.from(state == ActiveEnum.ACTIVE)); if (homekitTimer) { if ((state == ActiveEnum.ACTIVE)) { @@ -160,42 +158,37 @@ public CompletableFuture setValveActive(ActiveEnum state) { // let home app refresh the remaining duration ((GenericItem) getRootAccessory().getItem()).send(RefreshType.REFRESH); } - } + }); return CompletableFuture.completedFuture(null); } private void switchOffValve() { - SwitchItem item = getItem(HomekitCharacteristicType.ACTIVE_STATUS, SwitchItem.class); - if (item != null) { - item.send(OnOffType.OFF); - } + getItem(ACTIVE_STATUS, SwitchItem.class).ifPresent(item -> item.send(OnOffType.OFF)); } @Override public void subscribeValveActive(HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.ACTIVE_STATUS, callback); + subscribe(ACTIVE_STATUS, callback); } @Override public void unsubscribeValveActive() { - unsubscribe(HomekitCharacteristicType.ACTIVE_STATUS); + unsubscribe(ACTIVE_STATUS); } @Override public CompletableFuture getValveInUse() { - return CompletableFuture - .completedFuture((this.inUseReader.getValue() != null && this.inUseReader.getValue()) ? InUseEnum.IN_USE - : InUseEnum.NOT_IN_USE); + return CompletableFuture.completedFuture(inUseReader.getValue() ? InUseEnum.IN_USE : InUseEnum.NOT_IN_USE); } @Override public void subscribeValveInUse(HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.INUSE_STATUS, callback); + subscribe(INUSE_STATUS, callback); } @Override public void unsubscribeValveInUse() { - unsubscribe(HomekitCharacteristicType.INUSE_STATUS); + unsubscribe(INUSE_STATUS); } @Override diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitWindowCoveringImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitWindowCoveringImpl.java index 3cecde71c6dc9..f48b5d665c0d4 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitWindowCoveringImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitWindowCoveringImpl.java @@ -12,14 +12,15 @@ */ package org.openhab.io.homekit.internal.accessories; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.CURRENT_POSITION; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.TARGET_POSITION; + import java.util.List; import java.util.concurrent.CompletableFuture; -import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.library.items.RollershutterItem; import org.eclipse.smarthome.core.library.types.PercentType; import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; -import org.openhab.io.homekit.internal.HomekitCharacteristicType; import org.openhab.io.homekit.internal.HomekitSettings; import org.openhab.io.homekit.internal.HomekitTaggedItem; @@ -35,14 +36,14 @@ public class HomekitWindowCoveringImpl extends AbstractHomekitAccessoryImpl implements WindowCoveringAccessory { public HomekitWindowCoveringImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, - HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { + HomekitAccessoryUpdater updater, HomekitSettings settings) { super(taggedItem, mandatoryCharacteristics, updater, settings); this.getServices().add(new WindowCoveringService(this)); } @Override public CompletableFuture getCurrentPosition() { - PercentType value = getStateAs(HomekitCharacteristicType.CURRENT_POSITION, PercentType.class); + PercentType value = getStateAs(CURRENT_POSITION, PercentType.class); return CompletableFuture.completedFuture(value != null ? 100 - value.intValue() : 100); } @@ -57,18 +58,14 @@ public CompletableFuture getTargetPosition() { } @Override - public CompletableFuture setTargetPosition(int value) throws Exception { - final @Nullable RollershutterItem item = getItem(HomekitCharacteristicType.TARGET_POSITION, - RollershutterItem.class); - if (item != null) { - item.send(new PercentType(100 - value)); - } + public CompletableFuture setTargetPosition(int value) { + getItem(TARGET_POSITION, RollershutterItem.class).ifPresent(item -> item.send(new PercentType(100 - value))); return CompletableFuture.completedFuture(null); } @Override public void subscribeCurrentPosition(HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.CURRENT_POSITION, callback); + subscribe(CURRENT_POSITION, callback); } @Override @@ -78,12 +75,12 @@ public void subscribePositionState(HomekitCharacteristicChangeCallback callback) @Override public void subscribeTargetPosition(HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.TARGET_POSITION, callback); + subscribe(TARGET_POSITION, callback); } @Override public void unsubscribeCurrentPosition() { - unsubscribe(HomekitCharacteristicType.CURRENT_POSITION); + unsubscribe(CURRENT_POSITION); } @Override @@ -93,6 +90,6 @@ public void unsubscribePositionState() { @Override public void unsubscribeTargetPosition() { - unsubscribe(HomekitCharacteristicType.CURRENT_POSITION); + unsubscribe(CURRENT_POSITION); } } diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/IncompleteAccessoryException.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/IncompleteAccessoryException.java index 184f735d6ba2e..15fb272759b9f 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/IncompleteAccessoryException.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/IncompleteAccessoryException.java @@ -12,12 +12,14 @@ */ package org.openhab.io.homekit.internal.accessories; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.io.homekit.internal.HomekitCharacteristicType; /** * * @author Tim Harper - Initial contribution */ +@NonNullByDefault public class IncompleteAccessoryException extends Exception { private static final long serialVersionUID = 8595808359805444177L; From b5c5e3945dcf3e566ec2e0cbfe8c8ee543b64b9d Mon Sep 17 00:00:00 2001 From: Jan Gustafsson Date: Thu, 9 Jul 2020 10:13:42 +0200 Subject: [PATCH 46/85] [verisure] Verisure Binding initial contribution (#4789) Also-by: Jarle Hjortland (github: jarlebh) Signed-off-by: Jan Gustafsson (github: jannegpriv) --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../org.openhab.binding.verisure/.classpath | 32 + bundles/org.openhab.binding.verisure/.project | 23 + bundles/org.openhab.binding.verisure/NOTICE | 20 + .../org.openhab.binding.verisure/README.md | 585 ++++++++++ bundles/org.openhab.binding.verisure/pom.xml | 26 + .../src/main/feature/feature.xml | 10 + .../internal/DeviceStatusListener.java | 41 + .../internal/VerisureBindingConstants.java | 165 +++ .../internal/VerisureBridgeConfiguration.java | 30 + .../internal/VerisureHandlerFactory.java | 127 +++ .../verisure/internal/VerisureSession.java | 1012 +++++++++++++++++ .../internal/VerisureThingConfiguration.java | 47 + .../VerisureThingDiscoveryService.java | 129 +++ .../internal/dto/VerisureAlarmsDTO.java | 213 ++++ .../internal/dto/VerisureBaseThingDTO.java | 571 ++++++++++ .../dto/VerisureBroadbandConnectionsDTO.java | 130 +++ .../internal/dto/VerisureClimatesDTO.java | 187 +++ .../internal/dto/VerisureDoorWindowsDTO.java | 169 +++ .../internal/dto/VerisureEventLogDTO.java | 347 ++++++ .../internal/dto/VerisureGatewayDTO.java | 173 +++ .../dto/VerisureInstallationsDTO.java | 300 +++++ .../dto/VerisureMiceDetectionDTO.java | 279 +++++ .../internal/dto/VerisureSmartLockDTO.java | 185 +++ .../internal/dto/VerisureSmartLocksDTO.java | 222 ++++ .../internal/dto/VerisureSmartPlugsDTO.java | 154 +++ .../internal/dto/VerisureThingDTO.java | 48 + .../dto/VerisureUserPresencesDTO.java | 247 ++++ .../handler/VerisureAlarmThingHandler.java | 213 ++++ .../handler/VerisureBridgeHandler.java | 266 +++++ ...risureBroadbandConnectionThingHandler.java | 69 ++ .../VerisureClimateDeviceThingHandler.java | 109 ++ .../VerisureDoorWindowThingHandler.java | 94 ++ .../handler/VerisureEventLogThingHandler.java | 174 +++ .../handler/VerisureGatewayThingHandler.java | 106 ++ .../VerisureMiceDetectionThingHandler.java | 130 +++ .../VerisureSmartLockThingHandler.java | 379 ++++++ .../VerisureSmartPlugThingHandler.java | 218 ++++ .../handler/VerisureThingHandler.java | 355 ++++++ .../VerisureUserPresenceThingHandler.java | 101 ++ .../resources/ESH-INF/binding/binding.xml | 9 + .../resources/ESH-INF/thing/thing-types.xml | 804 +++++++++++++ bundles/pom.xml | 1 + 44 files changed, 8506 insertions(+) create mode 100644 bundles/org.openhab.binding.verisure/.classpath create mode 100644 bundles/org.openhab.binding.verisure/.project create mode 100644 bundles/org.openhab.binding.verisure/NOTICE create mode 100644 bundles/org.openhab.binding.verisure/README.md create mode 100644 bundles/org.openhab.binding.verisure/pom.xml create mode 100644 bundles/org.openhab.binding.verisure/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/DeviceStatusListener.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureBindingConstants.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureBridgeConfiguration.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureHandlerFactory.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureSession.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureThingConfiguration.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/discovery/VerisureThingDiscoveryService.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureAlarmsDTO.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureBaseThingDTO.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureBroadbandConnectionsDTO.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureClimatesDTO.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureDoorWindowsDTO.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureEventLogDTO.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureGatewayDTO.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureInstallationsDTO.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureMiceDetectionDTO.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureSmartLockDTO.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureSmartLocksDTO.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureSmartPlugsDTO.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureThingDTO.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureUserPresencesDTO.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureAlarmThingHandler.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureBridgeHandler.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureBroadbandConnectionThingHandler.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureClimateDeviceThingHandler.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureDoorWindowThingHandler.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureEventLogThingHandler.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureGatewayThingHandler.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureMiceDetectionThingHandler.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureSmartLockThingHandler.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureSmartPlugThingHandler.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureThingHandler.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureUserPresenceThingHandler.java create mode 100644 bundles/org.openhab.binding.verisure/src/main/resources/ESH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.verisure/src/main/resources/ESH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index 8f1f1b98f59a4..126740d2c5685 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -217,6 +217,7 @@ /bundles/org.openhab.binding.vektiva/ @octa22 /bundles/org.openhab.binding.velbus/ @cedricboon /bundles/org.openhab.binding.velux/ @gs4711 +/bundles/org.openhab.binding.verisure/ @jannegpriv /bundles/org.openhab.binding.vigicrues/ @clinique /bundles/org.openhab.binding.vitotronic/ @steand /bundles/org.openhab.binding.volvooncall/ @clinique diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index aaf42e9c9a9e9..755f6020995bf 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1084,6 +1084,11 @@ org.openhab.binding.velux ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.verisure + ${project.version} + org.openhab.addons.bundles org.openhab.binding.vigicrues diff --git a/bundles/org.openhab.binding.verisure/.classpath b/bundles/org.openhab.binding.verisure/.classpath new file mode 100644 index 0000000000000..615608997a6c5 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.verisure/.project b/bundles/org.openhab.binding.verisure/.project new file mode 100644 index 0000000000000..10bc8e87a0831 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.verisure + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.verisure/NOTICE b/bundles/org.openhab.binding.verisure/NOTICE new file mode 100644 index 0000000000000..0c13fa7419c02 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/NOTICE @@ -0,0 +1,20 @@ +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 + +== Third-party Content + +jsoup +* License: MIT License +* Project: https://jsoup.org/ +* Source: https://github.com/jhy/jsoup diff --git a/bundles/org.openhab.binding.verisure/README.md b/bundles/org.openhab.binding.verisure/README.md new file mode 100644 index 0000000000000..646264cd14236 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/README.md @@ -0,0 +1,585 @@ +# Verisure Binding + +This is an openHAB binding for Verisure Alarm System, by Securitas Direct. + +This binding uses the rest API behind the Verisure My Pages: + +https://mypages.verisure.com/login.html. + +Be aware that Verisure don't approve if you update to often, I have gotten no complaints running with a 10 minutes update interval, but officially you should use 30 minutes. + + +## Supported Things + +This binding supports the following thing types: + +- Bridge +- Alarm +- Smoke Detector (climate) +- Water Detector (climate) +- Siren (climate) +- Night Control +- Yaleman SmartLock +- SmartPlug +- Door/Window Status +- User Presence Status +- Broadband Connection Status +- Mice Detection Status (incl. climate) +- Event Log +- Gateway + + +## Binding Configuration + +You will have to configure the bridge with username and password, these must be the same credentials as used when logging into https://mypages.verisure.com. + +You must also configure your pin-code(s) to be able to lock/unlock the SmartLock(s) and arm/unarm the Alarm(s). + +**NOTE:** To be able to have full control over all SmartLock functionality, the user has to have Administrator rights. + +## Discovery + +After the configuration of the Verisure Bridge all of the available Sensors, Alarms, SmartPlugs, SmartLocks, Climate and Mice Detection devices will be discovered and placed as things in the inbox. + +## Thing Configuration + +Only the bridge require manual configuration. The devices and sensors can be added by hand, or you can let the discovery mechanism automatically find all of your Verisure things. + +## Enable Debugging + +To enable DEBUG logging for the binding, login to Karaf console and enter: + +`openhab> log:set DEBUG org.openhab.binding.verisure` + +## Supported Things and Channels + +### Verisure Bridge + +#### Configuration Options + +* `username` - The username used to connect to http://mypage.verisure.com + * The user has to have Administrator rights to have full SmartLock functionality + +* `password` - The password used to connect to http://mypage.verisure.com + +* `refresh` - Specifies the refresh interval in seconds + +* `pin` - The username's pin code to arm/disarm alarm and lock/unlock door. In the case of more than one installation and different pin-codes, use a comma separated string where pin-code matches order of installations. The installation order can be found using DEBUG log settings. + * Two installations where the first listed installation uses a 6 digit pin-code and second listed installation uses a 4 digit pin-code: 123456,1234 + +If you define the bridge in a things-file the bridge type id is defined as `bridge`, e.g.: + +`Bridge verisure:bridge:myverisureBridge verisure:bridge:myverisure` + +#### Channels + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|-----------------|-----------|-------------------------------------------------------------------------------------------------| +| status | String | This channel can be used to trigger an instant refresh by sending a RefreshType.REFRESH command.| + + +### Verisure Alarm + +#### Configuration Options + +* `deviceId` - Device Id + * Since Alarm lacks a Verisure ID, the following naming convention is used for alarm on installation ID 123456789: 'alarm123456789'. Installation ID can be found using DEBUG log settings + +#### Channels + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|---------------------|-----------|-------------------------------------------------------------------------------------------| +| changedByUser | String | This channel reports the user that last changed the state of the alarm. | +| changedVia | String | This channel reports the method used to change the status. | +| timestamp | DateTime | This channel reports the last time the alarm status was changed. | +| installationName | String | This channel reports the installation name. | +| installationId | Number | This channel reports the installation ID. | +| alarmStatus | String | This channel is used to arm/disarm the alarm. Available alarm status are "DISARMED", "ARMED_HOME" and "ARMED_AWAY".| +| alarmTriggerChannel | trigger | This is a trigger channel that receives events.| + +### Verisure Yaleman SmartLock + +#### Configuration Options + +* `deviceId` - Device Id + * Sensor Id. Example 5A4C35FT (Note: Verisure ID, found in the Verisure App or My Pages) + +#### Channels + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|-------------------------|-----------|----------------------------------------------------------------------------------------------------------| +| changedByUser | String | This channel reports the user that last changed the state of the alarm. | +| timestamp | DateTime | This channel reports the last time the alarm status was changed. | +| changedVia | String | This channel reports the method used to change the status. | +| motorJam | Switch | This channel reports if the SmartLock motor has jammed. | +| location | String | This channel reports the location of the device. | +| installationName | String | This channel reports the installation name. | +| installationId | Number | This channel reports the installation ID. | +| smartLockStatus | Switch | This channel is used to lock/unlock. | +| autoRelock | Switch | This channel is used to configure auto-lock functionality. Only supported for users with Administrator rights. | +| smartLockVolume | String | This channel is used to set the volume level. Available volume settings are "SILENCE", "LOW" and "HIGH". Only supported for users with Administrator rights.| +| smartLockVoiceLevel | String | This channel is used to set the voice level. Available voice level settings are "ESSENTIAL" and "NORMAL". Only supported for users with Administrator rights.| +| smartLockTriggerChannel | trigger | This is a trigger channel that receives events. | + +### Verisure SmartPlug + +#### Configuration Options + +* `deviceId` - Device Id + * Sensor Id. Example 5A4C35FT (Note: Verisure ID, found in the Verisure App or My Pages or on the sensor itself) + +#### Channels + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|-------------------------|-----------|-------------------------------------------------------------------| +| hazardous | Switch | This channel reports if the smart plug is configured as hazardous.| +| location | String | This channel reports the location of the device. | +| installationName | String | This channel reports the installation name. | +| installationId | Number | This channel reports the installation ID. | +| smartPlugStatus | Switch | This channel is used to turn smart plug on/off. | +| smartPlugTriggerChannel | trigger | This is a trigger channel that receives events. | + +### Verisure Smoke Detector + +#### Configuration Options + +* `deviceId` - Device Id + * Sensor Id. Example 5A4C35FT (Note: Verisure ID, found in the Verisure App or on the sensor itself) + +#### Channels + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|-----------------------------|-----------------------|-----------------------------------------------------------------------------| +| temperature | Number:Temperature | This channel reports the current temperature. | +| humidity | Number | This channel reports the current humidity in percentage. | +| humidityEnabled | Switch | This channel reports if the Climate is device capable of reporting humidity.| +| timestamp | DateTime | This channel reports the last time this sensor was updated. | +| location | String | This channel reports the location of the device. | +| installationName | String | This channel reports the installation name. | +| installationId | Number | This channel reports the installation ID. | +| smokeDetectorTriggerChannel | trigger | This is a trigger channel that receives events.| + +### Verisure Water Detector + +#### Configuration Options + +* `deviceId` - Device Id + * Sensor Id. Example 5A4C35FT (Note: Verisure ID, found in the Verisure App or My Pages or on the sensor itself) + +#### Channels + +The following channels are supported: + + +| Channel Type ID | Item Type | Description | +|-----------------------------|-----------------------|--------------------------------------------------------------| +| temperature | Number:Temperature | This channel reports the current temperature. | +| timestamp | DateTime | This channel reports the last time this sensor was updated. | +| location | String | This channel reports the location of the device. | +| installationName | String | This channel reports the installation name. | +| installationId | Number | This channel reports the installation ID. | +| waterDetectorTriggerChannel | trigger | This is a trigger channel that receives events. | + + +### Verisure Siren + +#### Configuration Options + +* `deviceId` - Device Id + * Sensor Id. Example 5A4C35FT (Note: Verisure ID, found in the Verisure App or My Pages or on the sensor itself) + +#### Channels + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|---------------------|-----------------------|------------------------------------------------------------| +| temperature | Number:Temperature | This channel reports the current temperature. | +| timestamp | DateTime | This channel reports the last time this sensor was updated.| +| location | String | This channel reports the location. | +| installationName | String | This channel reports the installation name. | +| installationId | Number | This channel reports the installation ID. | +| sirenTriggerChannel | trigger | This is a trigger channel that receives events. | + +### Verisure Night Control + +#### Configuration Options + +* `deviceId` - Device Id + * Sensor Id. Example 5A4C35FT (Note: Verisure ID, found in the Verisure App or My Pages or on the sensor itself) + +#### Channels + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|----------------------------|-----------------------|------------------------------------------------------------| +| temperature | Number:Temperature | This channel reports the current temperature. | +| timestamp | DateTime | This channel reports the last time this sensor was updated.| +| location | String | This channel reports the location. | +| installationName | String | This channel reports the installation name. | +| installationId | Number | This channel reports the installation ID. | +| nightControlTriggerChannel | trigger | This is a trigger channel that receives events. | + +### Verisure DoorWindow Sensor + +#### Configuration Options + +* `deviceId` - Device Id + * Sensor Id. Example 5A4C35FT (Note: Verisure ID, found in the Verisure App or My Pages or on the sensor itself) + +#### Channels + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|--------------------------|-----------|-----------------------------------------------------------------------------| +| state | Contact | This channel reports the if the door/window is open or closed (OPEN/CLOSED).| +| timestamp | DateTime | This channel reports the last time this sensor was updated. | +| location | String | This channel reports the location of the device. | +| installationName | String | This channel reports the installation name. | +| installationId | Number | This channel reports the installation ID. | +| doorWindowTriggerChannel | trigger | This is a trigger channel that receives events. | + + +### Verisure User Presence + +#### Configuration Options + +* `deviceId` - Device Id + * Since User presence lacks a Verisure ID, it is constructed from the user's email address, where the '@' sign is removed, and the site id. The following naming convention is used for User presence on site id 123456789 for a user with email address test@gmail.com: 'uptestgmailcom123456789'. Installation ID can be found using DEBUG log settings. + +#### Channels + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|--------------------|-----------|-------------------------------------------------------------------------| +| userLocationStatus | String | This channel reports the user presence status (HOME/AWAY). | +| timestamp | DateTime | This channel reports the last time the User Presence status was changed.| +| userName | String | This channel reports the user's name. | +| webAccount | String | This channel reports the user's email address. | +| userDeviceName | String | This channel reports the name of the user device. | +| installationName | String | This channel reports the installation name. | +| installationId | Number | This channel reports the installation ID. | + +### Verisure Broadband Connection + +#### Configuration Options + +* `deviceId` - Device Id + * Since Broadband connection lacks a Verisure ID, the following naming convention is used for Broadband connection on site id 123456789: 'bc123456789'. Installation ID can be found using DEBUG log settings. + +#### Channels + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|-----------------|-----------|--------------------------------------------------------------------------------| +| connected | String | This channel reports the broadband connection status (true means connected). | +| timestamp | DateTime | This channel reports the last time the Broadband connection status was checked.| +| installationName| String | This channel reports the installation name. | +| installationId | Number | This channel reports the installation ID. | + +### Verisure Mice Detection + +#### Configuration Options + +* `deviceId` - Device Id + * Sensor Id. Example 5A4C35FT (Note: Verisure ID, found in the Verisure App or My Pages or on the sensor itself) + +#### Channels + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|-----------------------------|--------------------|-------------------------------------------------------------------------------------| +| countLatestDetection | Number | This channel reports the number of mice counts the latest detection during last 24. | +| countLast24Hours | Number | This channel reports the total number of mice counts the last 24h. | +| durationLatestDetection | Number:Time | This channel reports the detection duration in min of latest detection. | +| durationLast24Hours | Number:Time | This channel reports the total detection duration in min for the last 24 hours. | +| timestamp | DateTime | This channel reports time for the last mouse detection. | +| temperature | Number:Temperature | This channel reports the current temperature. | +| temperatureTimestamp | DateTime | This channel reports the time for the last temperature reading. | +| location | String | This channel reports the location of the device. | +| installationName | String | This channel reports the installation name. | +| installationId | Number | This channel reports the installation ID. | +| miceDetectionTriggerChannel | trigger | This is a trigger channel that receives events. | + +### Verisure Event Log + +#### Configuration Options + +* `deviceId` - Device Id + * Since Event Log lacks a Verisure ID, the following naming convention is used for Event Log on site id 123456789: 'el123456789'. Installation ID can be found using DEBUG log settings. + + +#### Channels + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|---------------------|-----------|-------------------------------------------------------------------------| +| lastEventLocation | String | This channel reports location for last event in event log. | +| lastEventDeviceId | String | This channel reports device ID for last event in event log. | +| lastEventDeviceType | String | This channel reports device type for last event in event log. | +| lastEventType | String | This channel reports type for last event in event log. | +| lastEventCategory | String | This channel reports category for last event in event log. | +| lastEventTime | DateTime | This channel reports time for last event in event log. | +| lastEventUserName | String | This channel reports user name for last event in event log. | +| eventLog | String | This channel reports the last 15 events from event log in a JSON array. | + +### Verisure Gateway + +#### Configuration Options + +* `deviceId` - Device Id + * Sensor Id. Example 3B4C35FT (Note: Verisure ID, found in the Verisure App or My Pages or on the Gateway itself) + +#### Channels + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|---------------------|-----------|----------------------------------------------------------------------| +| model | String | This channel reports gateway model. | +| location | String | This channel reports gateway location. | +| statusGSMOverUDP | String | This channel reports communication status for GSM over UDP. | +| testTimeGSMOverUDP | DateTime | This channel reports last communication test time for GSM over UDP. | +| statusGSMOverSMS | String | This channel reports communication status for GSM over SMS. | +| testTimeGSMOverSMS | DateTime | This channel reports last communication test time for GSM over SMS. | +| statusGPRSOverUDP | String | This channel reports communication status for GPRS over UDP. | +| testTimeGPRSOverUDP | DateTime | This channel reports last communication test time for GPRS over UDP. | +| statusETHOverUDP | String | This channel reports communication status for ETH over UDP. | +| testTimeETHOverUDP | DateTime | This channel reports last communication test time for ETH over UDP. | + +## Trigger Events + +To be able to get trigger events you need an active Event Log thing, you can either get it via auto-detection or create your own in a things-file. +The following trigger events are defined per thing type: + +| Event | Thing Type | Description | +|-------------------|---------------|------------------------------------------------------------| +| LOCK | SmartLock | SmartLock has been locked. | +| UNLOCK | SmartLock | SmartLock has been locked. | +| LOCK_FAILURE | SmartLock | SmartLock has failed to lock/unlock. | +| ARM | Alarm | Alarm has been armed. | +| DISARM | Alarm | Alarm has been disarmed. | +| DOORWINDOW_OPENED | DoorWindow | DoorWindow has detected a door/window that opened. | +| DOORWINDOW_CLOSED | DoorWindow | DoorWindow has detected a door/window that closed. | +| INTRUSION | DoorWindow | DoorWindow has detected an intrusion. | +| FIRE | SmokeDetector | SmokeDetector has detected fire/smoke. | +| WATER | WaterDetector | WaterDetector has detected a water leak. | +| MICE | MiceDetector | WaterMiceDetector has detected a mouse. | +| COM_FAILURE | All | Communication failure detected. | +| COM_RESTORED | All | Communication restored. | +| COM_TEST | All | Communication test. | +| BATTERY_LOW | All | Battery low level detected. | +| BATTERY_RESTORED | All | Battery level restored. | +| SABOTAGE_ALARM | All | Sabotage alarm detected. | +| SABOTAGE_RESTORED | All | Sabotage alarm restored. | + +## Example + +### Things-file + +```` +// Bridge configuration +Bridge verisure:bridge:myverisure "Verisure Bridge" [username="x@y.com", password="1234", refresh="600", pin="111111"] { + + Thing alarm JannesAlarm "Verisure Alarm" [ deviceId="alarm123456789" ] + Thing smartLock JannesSmartLock "Verisure Entrance Yale Doorman" [ deviceId="3C446NPO" ] + Thing smartPlug JannesSmartPlug "Verisure SmartPlug" [ deviceId="3D7GMANV" ] + Thing waterDetector JannesWaterDetector "Verisure Water Detector" [ deviceId="3WETQRH5" ] + Thing userPresence JannesUserPresence "Verisure User Presence" [ deviceId="uptestgmailcom123456789" ] + Thing eventLog JannesEventLog "Verisure Event Log" [ deviceId="el123456789" ] + Thing gateway JannesGateway "Verisure Gateway" [ deviceId="3AFG5673" ] +} +```` + +### Items-file + +```` +Group gVerisureMiceDetection +Group gVerisureEventLog +Group gVerisureGateway + +// SmartLock and Alarm +Switch SmartLock "Verisure SmartLock" [ "Switchable" ] {channel="verisure:smartLock:myverisure:JannesSmartLock:smartLockStatus"} +Switch AutoLock "AutoLock" [ "Switchable" ] {channel="verisure:smartLock:myverisure:JannesSmartLock:autoRelock"} +String SmartLockVolume "SmartLock Volume" {channel="verisure:smartLock:myverisure:JannesSmartLock:smartLockVolume"} +DateTime SmartLockLastUpdated "SmartLock Last Updated [%1$tY-%1$tm-%1$td %1$tR]" {channel="verisure:smartLock:myverisure:JannesSmartLock:timestamp"} +String AlarmHome "Alarm Home" {channel="verisure:alarm:myverisure:JannesAlarm:alarmStatus"} +DateTime AlarmLastUpdated "Verisure Alarm Last Updated [%1$tY-%1$tm.%1$td %1$tR]" {channel="verisure:alarm:myverisure:JannesAlarm:timestamp"} +String AlarmChangedByUser "Verisure Alarm Changed By User" {channel="verisure:alarm:myverisure:JannesAlarm:changedByUser"} + + +// SmartPlugs +Switch SmartPlugLamp "SmartPlug" [ "Switchable" ] {channel="verisure:smartPlug:myverisure:4ED5ZXYC:smartPlugStatus"} +Switch SmartPlugGlavaRouter "SmartPlug Glava Router" [ "Switchable" ] {channel="verisure:smartPlug:myverisure:JannesSmartPlug:smartPlugStatus"} + +// DoorWindow +String DoorWindowLocation "Door Window Location" {channel="verisure:doorWindowSensor:myverisure:1SG5GHGT:location"} +String DoorWindowStatus "Door Window Status" {channel="verisure:doorWindowSensor:myverisure:1SG5GHGT:state"} + +// UserPresence +String UserName "User Name" {channel="verisure:userPresence:myverisure:JannesUserPresence:userName"} +String UserLocationEmail "User Location Email" {channel="verisure:userPresence:myverisure:JannesUserPresence:webAccount"} +String UserLocationName "User Location Name" {channel="verisure:userPresence:myverisure:JannesUserPresence:userLocationStatus"} +String UserNameGlava "User Name Glava" {channel="verisure:userPresence:myverisure:userpresencetestgmailcom123456789:userName"} +String UserLocationEmailGlava "User Location Email Glava" {channel="verisure:userPresence:myverisure:userpresencetestgmailcom123456789:webAccount"} +String UserLocationNameGlava "User Location Name Glava" {channel="verisure:userPresence:myverisure:userpresencetestgmailcom1123456789:userLocationStatus"} + +// EventLog +String LastEventLocation "Last Event Location" (gVerisureEventLog) {channel="verisure:eventLog:myverisure:JannesEventLog:lastEventLocation"} +String LastEventDeviceId "Last Event Device ID" (gVerisureEventLog) {channel="verisure:eventLog:myverisure:JannesEventLog:lastEventDeviceId"} +String LastEventDeviceType "Last Event Device Type" (gVerisureEventLog) {channel="verisure:eventLog:myverisure:JannesEventLog:lastEventDeviceType"} +String LastEventType "Last Event Type" (gVerisureEventLog) {channel="verisure:eventLog:myverisure:JannesEventLog:lastEventType"} +String LastEventCategory "Last Event Category" (gVerisureEventLog) {channel="verisure:eventLog:myverisure:JannesEventLog:lastEventCategory"} +DateTime LastEventTime "Last Event Time [%1$tY-%1$tm-%1$td %1$tR]" (gVerisureEventLog) {channel="verisure:eventLog:myverisure:JannesEventLog:lastEventTime"} +String LastEventUserName "Last Event User Name" (gVerisureEventLog) {channel="verisure:eventLog:myverisure:JannesEventLog:lastEventUserName"} +String EventLog "Event Log" {channel="verisure:eventLog:myverisure:JannesEventLog:eventLog"} + +// Gateway +String VerisureGatewayModel "Gateway Model" (gVerisureGateway) {channel="verisure:gateway:myverisure:JannesGateway:model"} +String VerisureGatewayLocation "Gateway Location" (gVerisureGateway) {channel="verisure:gateway:myverisure:JannesGateway:location"} +String VerisureGWStatusGSMOverUDP "Gateway Status GSMOverUDP" (gVerisureGateway) {channel="verisure:gateway:myverisure:JannesGateway:statusGSMOverUDP"} +DateTime VerisureGWTestTimeGSMOverUDP "Gateway Test Time GSMOverUDP" (gVerisureGateway) {channel="verisure:gateway:myverisure:JannesGateway:testTimeGSMOverUDP"} +String VerisureGWStatusGSMOverSMS "Gateway Status GSMOverSMS" (gVerisureGateway) {channel="verisure:gateway:myverisure:JannesGateway:statusGSMOverSMS"} +DateTime VerisureGWTestTimeGSMOverSMS "Gateway Test Time GSMOverSMS" (gVerisureGateway) {channel="verisure:gateway:myverisure:JannesGateway:testTimeGSMOverSMS"} +String VerisureGWStatusGPRSOverUDP "Gateway Status GPRSOverUDP" (gVerisureGateway) {channel="verisure:gateway:myverisure:JannesGateway:statusGPRSOverUDP"} +DateTime VerisureGWTestTimeGPRSOverUDP "Gateway Test Time GPRSOverUDP" (gVerisureGateway) {channel="verisure:gateway:myverisure:JannesGateway:testTimeGPRSOverUDP"} +String VerisureGWStatusETHOverUDP "Gateway Status ETHOverUDP" (gVerisureGateway) {channel="verisure:gateway:myverisure:JannesGateway:statusETHOverUDP"} +DateTime VerisureGWTestTimeETHOverUDP "Gateway Test Time ETHOverUDP" (gVerisureGateway) {channel="verisure:gateway:myverisure:JannesGateway:testTimeETHOverUDP"} + +// Broadband Connection +String CurrentBBStatus "Broadband Connection Status" {channel="verisure:broadbandConnection:myverisure:bc123456789:connected"} + +// Verisure Mice Detection +Number MouseCountLastDetection "Mouse Count Last Detection" (gVerisureMiceDetection) {channel="verisure:miceDetection:myverisure:2CFZH80U:countLatestDetection"} +Number MouseCountLast24Hours "Mouse Count Last 24 Hours" (gVerisureMiceDetection) {channel="verisure:miceDetection:myverisure:2CFZH80U:countLast24Hours"} +DateTime MouseLastDetectionTime "Mouse Last Detection Time [%1$tY-%1$tm-%1$td %1$tR]" (gVerisureMiceDetection) {channel="verisure:miceDetection:myverisure:2CFZH80U:timestamp"} +Number MouseDurationLastDetection "Mouse Duration Last Detection" (gVerisureMiceDetection) {channel="verisure:miceDetection:myverisure:2CFZH80U:durationLatestDetection"} +Number MouseDurationLast24Hours "Mouse Duration Last 24 Hours" (gVerisureMiceDetection) {channel="verisure:miceDetection:myverisure:2CFZH80U:durationLast24Hours"} +Number MouseDetectionTemperature "Mouse Detection Temperature [%.1f C]" (gTemperaturesVerisure, gVerisureMiceDetection) ["CurrentTemperature"] {channel="verisure:miceDetection:myverisure:2CFZH80U:temperature"} +DateTime MouseDetectionTemperatureTime "Mouse Detection Temperature Time [%1$tY-%1$tm-%1$td %1$tR]" (gVerisureMiceDetection) {channel="verisure:miceDetection:myverisure:2CFZH80U:temperatureTimestamp"} +String MouseDetectionLocation "Mouse Detection Location" (gVerisureMiceDetection) {channel="verisure:miceDetection:myverisure:2CFZH80U:location"} + +```` + +### Sitemap + +```` + Frame label="SmartLock and Alarm" { + Text label="SmartLock and Alarm" icon="groundfloor" { + Frame label="Yale Doorman SmartLock" { + Switch item=SmartLock label="Yale Doorman SmartLock" icon="lock.png" + } + Frame label="Verisure Alarm" { + Switch item=AlarmHome icon="alarm" label="Verisure Alarm" mappings=["DISARMED"="Disarm", "ARMED_HOME"="Arm Home", "ARMED_AWAY"="Arm Away"] + } + Frame label="Yale Doorman SmartLock AutoLock" { + Switch item=AutoLock label="Yale Doorman SmartLock AutoLock" icon="lock.png" + } + Frame label="Yale Doorman SmartLock Volume" { + Switch item=SmartLockVolume icon="lock" label="Yale Doorman SmartLock Volume" mappings=["SILENCE"="Silence", "LOW"="Low", "HIGH"="High"] + } + Text item=AlarmHomeInstallationName label="Alarm Installation [%s]" + Text item=AlarmChangedByUser label="Changed by user [%s]" + Text item=AlarmLastUpdated + Text item=SmartLockStatus label="SmartLock status [%s]" + Text item=SmartLockLastUpdated + Text item=SmartLockOperatedBy label="Changed by user [%s]" + Text item=DoorWindowStatus label="Door State" + Text item=DoorWindowLocation + } + } + + Frame label="SmartPlugs" { + Text label="SmartPlugs" icon="attic" { + Frame label="SmartPlug Lamp" { + Switch item=SmartPlugLamp label="Verisure SmartPlug Lamp" icon="smartheater.png" + } + } + } + + Frame label="User Presence" { + Text label="User Presence" icon="attic" { + Frame label="User Presence Champinjonvägen" { + Text item=UserName label="User Name [%s]" + Text item=UserLocationEmail label="User Email [%s]" + Text item=UserLocationStatus label="User Location Status [%s]" + } + } + } + + Frame label="Broadband Connection" { + Text label="Broadband Connection" icon="attic" { + Frame label="Broadband Connection Champinjonvägen" { + Text item=CurrentBBStatus label="Broadband Connection Status [%s]" + } + } + } + + Frame label="Mice Detection" { + Group item=gVerisureMiceDetection label="Verisure Mice Detection" + } + + Frame label="Event Log" { + Group item=gVerisureEventLog label="Verisure Event Log" + } + + Frame label="Gateway" { + Group item=gVerisureGateway label="Verisure Gateway" + } + +```` + +### Rules + +```` +import org.eclipse.smarthome.core.types.RefreshType + +rule "Handle Refesh of Verisure" +when + Item RefreshVerisure received command +then + var String command = RefreshVerisure.state.toString.toLowerCase + logDebug("RULES","RefreshVerisure Rule command: " + command) + sendCommand(VerisureBridgeStatus, RefreshType.REFRESH) +end + +rule "Verisure SmartLock Event Triggers" +when + Channel "verisure:smartLock:myverisure:JannesSmartLock:smartLockTriggerChannel" triggered +then + logInfo("RULES", "A SmartLock trigger event was detected:" + receivedEvent.toString()) +end + +rule "Verisure Gateway Event Triggers" +when + Channel "verisure:gateway:myverisure:JannesGateway:gatewayTriggerChannel" triggered +then + logInfo("RULES", "A Gateway trigger event was detected:" + receivedEvent.toString()) +end + +rule "Verisure DoorWindow Event Triggers" +when + Channel "verisure:doorWindowSensor:myverisure:1SG5GHGT:doorWindowTriggerChannel" triggered +then + logInfo("RULES", "A DoorWindow trigger event was detected:" + receivedEvent.toString()) +end + + +```` + diff --git a/bundles/org.openhab.binding.verisure/pom.xml b/bundles/org.openhab.binding.verisure/pom.xml new file mode 100644 index 0000000000000..f20a67e2624b9 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.7-SNAPSHOT + + + org.openhab.binding.verisure + + openHAB Add-ons :: Bundles :: Verisure Binding + + + + org.jsoup + jsoup + 1.8.3 + compile + + + + diff --git a/bundles/org.openhab.binding.verisure/src/main/feature/feature.xml b/bundles/org.openhab.binding.verisure/src/main/feature/feature.xml new file mode 100644 index 0000000000000..64a788f07f1d5 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + openhab-runtime-base + openhab-transport-serial + mvn:org.jsoup/jsoup/1.8.3 + mvn:org.openhab.addons.bundles/org.openhab.binding.verisure/${project.version} + + diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/DeviceStatusListener.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/DeviceStatusListener.java new file mode 100644 index 0000000000000..fe8e55a718f17 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/DeviceStatusListener.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.verisure.internal.dto.VerisureThingDTO; + +/** + * The {@link DeviceStatusListener} is notified when a device status has changed + * or a device has been removed or added. + * + * @author Jarle Hjortland - Initial contribution + * @author Jan Gustafsson - Updated after code review comments + * + */ +@NonNullByDefault +public interface DeviceStatusListener { + + /** + * This method is called whenever the state of the given device has changed. + * + * @param thing + * The thing that was changed. + */ + void onDeviceStateChanged(T thing); + + /** + * This method returns the thing's class + */ + public Class getVerisureThingClass(); +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureBindingConstants.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureBindingConstants.java new file mode 100644 index 0000000000000..d4b10d5c9157f --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureBindingConstants.java @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The {@link VerisureBinding} class defines common constants, which are + * used across the whole binding. + * + * @author l3rum - Initial contribution + * @author Jan Gustafsson - Furher development + */ +@NonNullByDefault +public class VerisureBindingConstants { + + public static final String BINDING_ID = "verisure"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge"); + public static final ThingTypeUID THING_TYPE_ALARM = new ThingTypeUID(BINDING_ID, "alarm"); + public static final ThingTypeUID THING_TYPE_SMARTPLUG = new ThingTypeUID(BINDING_ID, "smartPlug"); + public static final ThingTypeUID THING_TYPE_SMOKEDETECTOR = new ThingTypeUID(BINDING_ID, "smokeDetector"); + public static final ThingTypeUID THING_TYPE_WATERDETECTOR = new ThingTypeUID(BINDING_ID, "waterDetector"); + public static final ThingTypeUID THING_TYPE_SIREN = new ThingTypeUID(BINDING_ID, "siren"); + public static final ThingTypeUID THING_TYPE_DOORWINDOW = new ThingTypeUID(BINDING_ID, "doorWindowSensor"); + public static final ThingTypeUID THING_TYPE_USERPRESENCE = new ThingTypeUID(BINDING_ID, "userPresence"); + public static final ThingTypeUID THING_TYPE_SMARTLOCK = new ThingTypeUID(BINDING_ID, "smartLock"); + public static final ThingTypeUID THING_TYPE_BROADBAND_CONNECTION = new ThingTypeUID(BINDING_ID, + "broadbandConnection"); + public static final ThingTypeUID THING_TYPE_NIGHT_CONTROL = new ThingTypeUID(BINDING_ID, "nightControl"); + public static final ThingTypeUID THING_TYPE_MICE_DETECTION = new ThingTypeUID(BINDING_ID, "miceDetection"); + public static final ThingTypeUID THING_TYPE_EVENT_LOG = new ThingTypeUID(BINDING_ID, "eventLog"); + public static final ThingTypeUID THING_TYPE_GATEWAY = new ThingTypeUID(BINDING_ID, "gateway"); + + // List of all Channel ids + public static final String CHANNEL_NUMERIC_STATUS = "numericStatus"; + public static final String CHANNEL_TEMPERATURE = "temperature"; + public static final String CHANNEL_HUMIDITY = "humidity"; + public static final String CHANNEL_HUMIDITY_ENABLED = "humidityEnabled"; + public static final String CHANNEL_LOCATION = "location"; + public static final String CHANNEL_STATUS = "status"; + public static final String CHANNEL_CONNECTED = "connected"; + public static final String CHANNEL_STATE = "state"; + public static final String CHANNEL_LABEL = "label"; + public static final String CHANNEL_USER_NAME = "userName"; + public static final String CHANNEL_WEBACCOUNT = "webAccount"; + public static final String CHANNEL_USER_LOCATION_STATUS = "userLocationStatus"; + public static final String CHANNEL_USER_DEVICE_NAME = "userDeviceName"; + public static final String CHANNEL_SMARTLOCK_VOLUME = "smartLockVolume"; + public static final String CHANNEL_SMARTLOCK_VOICE_LEVEL = "smartLockVoiceLevel"; + public static final String CHANNEL_SMARTLOCK_TRIGGER_CHANNEL = "smartLockTriggerChannel"; + public static final String CHANNEL_AUTO_RELOCK = "autoRelock"; + public static final String CHANNEL_SMARTPLUG_STATUS = "smartPlugStatus"; + public static final String CHANNEL_SMARTPLUG_TRIGGER_CHANNEL = "smartPlugTriggerChannel"; + public static final String CHANNEL_ALARM_STATUS = "alarmStatus"; + public static final String CHANNEL_ALARM_TRIGGER_CHANNEL = "alarmTriggerChannel"; + public static final String CHANNEL_SMARTLOCK_STATUS = "smartLockStatus"; + public static final String CHANNEL_CHANGED_BY_USER = "changedByUser"; + public static final String CHANNEL_CHANGED_VIA = "changedVia"; + public static final String CHANNEL_TIMESTAMP = "timestamp"; + public static final String CHANNEL_TEMPERATURE_TIMESTAMP = "temperatureTimestamp"; + public static final String CHANNEL_HAZARDOUS = "hazardous"; + public static final String CHANNEL_MOTOR_JAM = "motorJam"; + public static final String CHANNEL_INSTALLATION_NAME = "installationName"; + public static final String CHANNEL_INSTALLATION_ID = "installationId"; + public static final String CHANNEL_COUNT_LATEST_DETECTION = "countLatestDetection"; + public static final String CHANNEL_COUNT_LAST_24_HOURS = "countLast24Hours"; + public static final String CHANNEL_DURATION_LATEST_DETECTION = "durationLatestDetection"; + public static final String CHANNEL_DURATION_LAST_24_HOURS = "durationLast24Hours"; + public static final String CHANNEL_LAST_EVENT_LOCATION = "lastEventLocation"; + public static final String CHANNEL_LAST_EVENT_ID = "lastEventId"; + public static final String CHANNEL_LAST_EVENT_DEVICE_ID = "lastEventDeviceId"; + public static final String CHANNEL_LAST_EVENT_DEVICE_TYPE = "lastEventDeviceType"; + public static final String CHANNEL_LAST_EVENT_TYPE = "lastEventType"; + public static final String CHANNEL_LAST_EVENT_CATEGORY = "lastEventCategory"; + public static final String CHANNEL_LAST_EVENT_TIME = "lastEventTime"; + public static final String CHANNEL_LAST_EVENT_USER_NAME = "lastEventUserName"; + public static final String CHANNEL_EVENT_LOG = "eventLog"; + public static final String CHANNEL_STATUS_GSM_OVER_UDP = "statusGSMOverUDP"; + public static final String CHANNEL_STATUS_GSM_OVER_SMS = "statusGSMOverSMS"; + public static final String CHANNEL_STATUS_GPRS_OVER_UDP = "statusGPRSOverUDP"; + public static final String CHANNEL_STATUS_ETH_OVER_UDP = "statusETHOverUDP"; + public static final String CHANNEL_TEST_TIME_GSM_OVER_UDP = "testTimeGSMOverUDP"; + public static final String CHANNEL_TEST_TIME_GSM_OVER_SMS = "testTimeGSMOverSMS"; + public static final String CHANNEL_TEST_TIME_GPRS_OVER_UDP = "testTimeGPRSOverUDP"; + public static final String CHANNEL_TEST_TIME_ETH_OVER_UDP = "testTimeETHOverUDP"; + public static final String CHANNEL_GATEWAY_MODEL = "model"; + public static final String CHANNEL_SMOKE_DETECTION_TRIGGER_CHANNEL = "smokeDetectionTriggerChannel"; + public static final String CHANNEL_MICE_DETECTION_TRIGGER_CHANNEL = "miceDetectionTriggerChannel"; + public static final String CHANNEL_WATER_DETECTION_TRIGGER_CHANNEL = "waterDetectionTriggerChannel"; + public static final String CHANNEL_SIREN_TRIGGER_CHANNEL = "sirenTriggerChannel"; + public static final String CHANNEL_NIGHT_CONTROL_TRIGGER_CHANNEL = "nightControlTriggerChannel"; + public static final String CHANNEL_DOOR_WINDOW_TRIGGER_CHANNEL = "doorWindowTriggerChannel"; + public static final String CHANNEL_GATEWAY_TRIGGER_CHANNEL = "gatewayTriggerChannel"; + + // Trigger channel events + public static final String TRIGGER_EVENT_LOCK = "LOCK"; + public static final String TRIGGER_EVENT_UNLOCK = "UNLOCK"; + public static final String TRIGGER_EVENT_LOCK_FAILURE = "LOCK_FAILURE"; + public static final String TRIGGER_EVENT_ARM = "ARM"; + public static final String TRIGGER_EVENT_DISARM = "DISARM"; + public static final String TRIGGER_EVENT_FIRE = "FIRE"; + public static final String TRIGGER_EVENT_INSTRUSION = "INTRUSION"; + public static final String TRIGGER_EVENT_WATER = "WATER"; + public static final String TRIGGER_EVENT_MICE = "MICE"; + public static final String TRIGGER_EVENT_BATTERY_LOW = "BATTERY_LOW"; + public static final String TRIGGER_EVENT_BATTERY_RESTORED = "BATTERY_RESTORED"; + public static final String TRIGGER_EVENT_COM_FAILURE = "COM_FAILURE"; + public static final String TRIGGER_EVENT_COM_RESTORED = "COM_RESTORED"; + public static final String TRIGGER_EVENT_COM_TEST = "COM_TEST"; + public static final String TRIGGER_EVENT_SABOTAGE_ALARM = "SABOTAGE_ALARM"; + public static final String TRIGGER_EVENT_SABOTAGE_RESTORED = "SABOTAGE_RESTORED"; + public static final String TRIGGER_EVENT_DOORWINDOW_OPENED = "DOORWINDOW_OPENED"; + public static final String TRIGGER_EVENT_DOORWINDOW_CLOSED = "DOORWINDOW_CLOSED"; + public static final String TRIGGER_EVENT_LOCATION_HOME = "LOCATION_HOME"; + public static final String TRIGGER_EVENT_LOCATION_AWAY = "LOCATION_AWAY"; + + // REST URI constants + public static final String USERNAME = "username"; + public static final String PASSWORD = "password"; + public static final String BASEURL = "https://mypages.verisure.com"; + public static final String LOGON_SUF = BASEURL + "/j_spring_security_check?locale=en_GB"; + public static final String ALARM_COMMAND = BASEURL + "/remotecontrol/armstatechange.cmd"; + public static final String SMARTLOCK_LOCK_COMMAND = BASEURL + "/remotecontrol/lockunlock.cmd"; + public static final String SMARTLOCK_SET_COMMAND = BASEURL + "/overview/setdoorlock.cmd"; + public static final String SMARTLOCK_AUTORELOCK_COMMAND = BASEURL + "/settings/setautorelock.cmd"; + public static final String SMARTLOCK_VOLUME_COMMAND = BASEURL + "/settings/setvolume.cmd"; + + public static final String SMARTPLUG_COMMAND = BASEURL + "/settings/smartplug/onoffplug.cmd"; + public static final String START_REDIRECT = "/uk/start.html"; + public static final String START_SUF = BASEURL + START_REDIRECT; + + // GraphQL constants + public static final String STATUS = BASEURL + "/uk/status"; + public static final String SETTINGS = BASEURL + "/uk/settings.html?giid="; + public static final String SET_INSTALLATION = BASEURL + "/setinstallation?giid="; + public static final String BASEURL_API = "https://m-api02.verisure.com"; + public static final String START_GRAPHQL = "/graphql"; + public static final String AUTH_TOKEN = "/auth/token"; + public static final String AUTH_LOGIN = "/auth/login"; + + public static final String ALARMSTATUS_PATH = "/remotecontrol"; + public static final String SMARTLOCK_PATH = "/overview/doorlock/"; + public static final String DOORWINDOW_PATH = "/settings/doorwindow"; + public static final String USERTRACKING_PATH = "/overview/usertrackingcontacts"; + public static final String CLIMATEDEVICE_PATH = "/overview/climatedevice"; + public static final String SMARTPLUG_PATH = "/settings/smartplug"; + public static final String ETHERNETSTATUS_PATH = "/overview/ethernetstatus"; + public static final String VACATIONMODE_PATH = "/overview/vacationmode"; + public static final String TEMPERATURE_CONTROL_PATH = "/overview/temperaturecontrol"; + public static final String MOUSEDETECTION_PATH = "/overview/mousedetection"; + public static final String CAMERA_PATH = "/overview/camera"; +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureBridgeConfiguration.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureBridgeConfiguration.java new file mode 100644 index 0000000000000..0c3f01481eaa5 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureBridgeConfiguration.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Configuration class for VerisureBridgeHandler bridge used to connect to the + * Verisure MyPage. + * + * @author Jarle Hjortland - Initial contribution + */ +@NonNullByDefault +public class VerisureBridgeConfiguration { + public @Nullable String username; + public @Nullable String password; + public int refresh; + public @Nullable String pin; +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureHandlerFactory.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureHandlerFactory.java new file mode 100644 index 0000000000000..0768b577fd5aa --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureHandlerFactory.java @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal; + +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.eclipse.smarthome.io.net.http.HttpClientFactory; +import org.openhab.binding.verisure.internal.handler.VerisureAlarmThingHandler; +import org.openhab.binding.verisure.internal.handler.VerisureBridgeHandler; +import org.openhab.binding.verisure.internal.handler.VerisureBroadbandConnectionThingHandler; +import org.openhab.binding.verisure.internal.handler.VerisureClimateDeviceThingHandler; +import org.openhab.binding.verisure.internal.handler.VerisureDoorWindowThingHandler; +import org.openhab.binding.verisure.internal.handler.VerisureEventLogThingHandler; +import org.openhab.binding.verisure.internal.handler.VerisureGatewayThingHandler; +import org.openhab.binding.verisure.internal.handler.VerisureMiceDetectionThingHandler; +import org.openhab.binding.verisure.internal.handler.VerisureSmartLockThingHandler; +import org.openhab.binding.verisure.internal.handler.VerisureSmartPlugThingHandler; +import org.openhab.binding.verisure.internal.handler.VerisureUserPresenceThingHandler; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link VerisureHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Jarle Hjortland - Initial contribution + * @author Jan Gustafsson - Further development + */ +@NonNullByDefault +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.verisure") +public class VerisureHandlerFactory extends BaseThingHandlerFactory { + + public static final Set SUPPORTED_THING_TYPES = new HashSet(); + static { + SUPPORTED_THING_TYPES.addAll(VerisureBridgeHandler.SUPPORTED_THING_TYPES); + SUPPORTED_THING_TYPES.addAll(VerisureAlarmThingHandler.SUPPORTED_THING_TYPES); + SUPPORTED_THING_TYPES.addAll(VerisureSmartLockThingHandler.SUPPORTED_THING_TYPES); + SUPPORTED_THING_TYPES.addAll(VerisureSmartPlugThingHandler.SUPPORTED_THING_TYPES); + SUPPORTED_THING_TYPES.addAll(VerisureClimateDeviceThingHandler.SUPPORTED_THING_TYPES); + SUPPORTED_THING_TYPES.addAll(VerisureBroadbandConnectionThingHandler.SUPPORTED_THING_TYPES); + SUPPORTED_THING_TYPES.addAll(VerisureDoorWindowThingHandler.SUPPORTED_THING_TYPES); + SUPPORTED_THING_TYPES.addAll(VerisureUserPresenceThingHandler.SUPPORTED_THING_TYPES); + SUPPORTED_THING_TYPES.addAll(VerisureMiceDetectionThingHandler.SUPPORTED_THING_TYPES); + SUPPORTED_THING_TYPES.addAll(VerisureEventLogThingHandler.SUPPORTED_THING_TYPES); + SUPPORTED_THING_TYPES.addAll(VerisureGatewayThingHandler.SUPPORTED_THING_TYPES); + } + + private final Logger logger = LoggerFactory.getLogger(VerisureHandlerFactory.class); + private final HttpClient httpClient; + + @Activate + public VerisureHandlerFactory(@Reference HttpClientFactory httpClientFactory) { + this.httpClient = httpClientFactory.getCommonHttpClient(); + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + logger.debug("createHandler this: {}", thing); + final ThingHandler thingHandler; + if (VerisureBridgeHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + logger.debug("Create VerisureBridgeHandler"); + thingHandler = new VerisureBridgeHandler((Bridge) thing, httpClient); + } else if (VerisureAlarmThingHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + logger.debug("Create VerisureAlarmThingHandler {}", thing.getThingTypeUID()); + thingHandler = new VerisureAlarmThingHandler(thing); + } else if (VerisureSmartLockThingHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + logger.debug("Create VerisureSmartLockThingHandler {}", thing.getThingTypeUID()); + thingHandler = new VerisureSmartLockThingHandler(thing); + } else if (VerisureSmartPlugThingHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + logger.debug("Create VerisureSmartPlugThingHandler {}", thing.getThingTypeUID()); + thingHandler = new VerisureSmartPlugThingHandler(thing); + } else if (VerisureClimateDeviceThingHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + logger.debug("Create VerisureClimateDeviceThingHandler {}", thing.getThingTypeUID()); + thingHandler = new VerisureClimateDeviceThingHandler(thing); + } else if (VerisureBroadbandConnectionThingHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + logger.debug("Create VerisureBroadbandConnectionThingHandler {}", thing.getThingTypeUID()); + thingHandler = new VerisureBroadbandConnectionThingHandler(thing); + } else if (VerisureDoorWindowThingHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + logger.debug("Create VerisureDoorWindowThingHandler {}", thing.getThingTypeUID()); + thingHandler = new VerisureDoorWindowThingHandler(thing); + } else if (VerisureUserPresenceThingHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + logger.debug("Create VerisureUserPresenceThingHandler {}", thing.getThingTypeUID()); + thingHandler = new VerisureUserPresenceThingHandler(thing); + } else if (VerisureMiceDetectionThingHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + logger.debug("Create VerisureMiceDetectionThingHandler {}", thing.getThingTypeUID()); + thingHandler = new VerisureMiceDetectionThingHandler(thing); + } else if (VerisureEventLogThingHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + logger.debug("Create VerisureEventLogThingHandler {}", thing.getThingTypeUID()); + thingHandler = new VerisureEventLogThingHandler(thing); + } else if (VerisureGatewayThingHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + logger.debug("Create VerisureGatewayThingHandler {}", thing.getThingTypeUID()); + thingHandler = new VerisureGatewayThingHandler(thing); + } else { + logger.debug("Not possible to create thing handler for thing {}", thing); + thingHandler = null; + } + return thingHandler; + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureSession.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureSession.java new file mode 100644 index 0000000000000..179425df21ced --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureSession.java @@ -0,0 +1,1012 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*; + +import java.math.BigDecimal; +import java.net.CookieStore; +import java.net.HttpCookie; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpResponseException; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.BytesContentProvider; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.openhab.binding.verisure.internal.dto.VerisureAlarmsDTO; +import org.openhab.binding.verisure.internal.dto.VerisureBroadbandConnectionsDTO; +import org.openhab.binding.verisure.internal.dto.VerisureClimatesDTO; +import org.openhab.binding.verisure.internal.dto.VerisureDoorWindowsDTO; +import org.openhab.binding.verisure.internal.dto.VerisureEventLogDTO; +import org.openhab.binding.verisure.internal.dto.VerisureGatewayDTO; +import org.openhab.binding.verisure.internal.dto.VerisureGatewayDTO.CommunicationState; +import org.openhab.binding.verisure.internal.dto.VerisureInstallationsDTO; +import org.openhab.binding.verisure.internal.dto.VerisureInstallationsDTO.Owainstallation; +import org.openhab.binding.verisure.internal.dto.VerisureMiceDetectionDTO; +import org.openhab.binding.verisure.internal.dto.VerisureSmartLockDTO; +import org.openhab.binding.verisure.internal.dto.VerisureSmartLocksDTO; +import org.openhab.binding.verisure.internal.dto.VerisureSmartPlugsDTO; +import org.openhab.binding.verisure.internal.dto.VerisureThingDTO; +import org.openhab.binding.verisure.internal.dto.VerisureUserPresencesDTO; +import org.openhab.binding.verisure.internal.handler.VerisureThingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +/** + * This class performs the communication with Verisure My Pages. + * + * @author Jarle Hjortland - Initial contribution + * @author Jan Gustafsson - Re-design and support for several sites and update to new Verisure API + * + */ +@NonNullByDefault +public class VerisureSession { + + @NonNullByDefault({}) + private final Map verisureThings = new ConcurrentHashMap<>(); + private final Map> verisureHandlers = new ConcurrentHashMap<>(); + private final Logger logger = LoggerFactory.getLogger(VerisureSession.class); + private final Gson gson = new Gson(); + private final List> deviceStatusListeners = new CopyOnWriteArrayList<>(); + private final Map verisureInstallations = new ConcurrentHashMap<>(); + private static final List APISERVERLIST = Arrays.asList("https://m-api01.verisure.com", + "https://m-api02.verisure.com"); + private int apiServerInUseIndex = 0; + private int numberOfEvents = 15; + private static final String USER_NAME = "username"; + private static final String PASSWORD_NAME = "vid"; + private String apiServerInUse = APISERVERLIST.get(apiServerInUseIndex); + private String authstring = ""; + private @Nullable String csrf; + private @Nullable String pinCode; + private HttpClient httpClient; + private @Nullable String userName = ""; + private @Nullable String password = ""; + + public VerisureSession(HttpClient httpClient) { + this.httpClient = httpClient; + } + + public boolean initialize(@Nullable String authstring, @Nullable String pinCode, @Nullable String userName) { + if (authstring != null) { + this.authstring = authstring.substring(0); + this.pinCode = pinCode; + this.userName = userName; + // Try to login to Verisure + if (logIn()) { + return getInstallations(); + } else { + return false; + } + } + return false; + } + + public boolean refresh() { + try { + if (logIn()) { + updateStatus(); + return true; + } else { + return false; + } + } catch (HttpResponseException e) { + logger.warn("Failed to do a refresh {}", e.getMessage()); + return false; + } + } + + public int sendCommand(String url, String data, BigDecimal installationId) { + logger.debug("Sending command with URL {} and data {}", url, data); + try { + configureInstallationInstance(installationId); + int httpResultCode = setSessionCookieAuthLogin(); + if (httpResultCode == HttpStatus.OK_200) { + return postVerisureAPI(url, data); + } else { + return httpResultCode; + } + } catch (ExecutionException | InterruptedException | TimeoutException e) { + logger.debug("Failed to send command {}", e.getMessage()); + } + return HttpStatus.BAD_REQUEST_400; + } + + public boolean unregisterDeviceStatusListener( + DeviceStatusListener deviceStatusListener) { + return deviceStatusListeners.remove(deviceStatusListener); + } + + @SuppressWarnings("unchecked") + public boolean registerDeviceStatusListener(DeviceStatusListener deviceStatusListener) { + return deviceStatusListeners.add((DeviceStatusListener) deviceStatusListener); + } + + @SuppressWarnings({ "unchecked" }) + public @Nullable T getVerisureThing(String deviceId, Class thingType) { + VerisureThingDTO thing = verisureThings.get(deviceId); + if (thingType.isInstance(thing)) { + return (T) thing; + } + return null; + } + + public @Nullable T getVerisureThing(String deviceId) { + VerisureThingDTO thing = verisureThings.get(deviceId); + if (thing != null) { + @SuppressWarnings("unchecked") + T thing2 = (T) thing; + return thing2; + } + return null; + } + + public @Nullable VerisureThingHandler getVerisureThinghandler(String deviceId) { + VerisureThingHandler thingHandler = verisureHandlers.get(deviceId); + return thingHandler; + } + + public void setVerisureThingHandler(VerisureThingHandler vth, String deviceId) { + verisureHandlers.put(deviceId, vth); + }; + + public void removeVerisureThingHandler(String deviceId) { + verisureHandlers.remove(deviceId); + } + + public Collection getVerisureThings() { + return verisureThings.values(); + } + + public @Nullable String getCsrf() { + return csrf; + } + + public @Nullable String getPinCode() { + return pinCode; + } + + public String getApiServerInUse() { + return apiServerInUse; + } + + public void setApiServerInUse(String apiServerInUse) { + this.apiServerInUse = apiServerInUse; + } + + public String getNextApiServer() { + apiServerInUseIndex++; + if (apiServerInUseIndex > (APISERVERLIST.size() - 1)) { + apiServerInUseIndex = 0; + } + return APISERVERLIST.get(apiServerInUseIndex); + } + + public void setNumberOfEvents(int numberOfEvents) { + this.numberOfEvents = numberOfEvents; + } + + public void configureInstallationInstance(BigDecimal installationId) + throws ExecutionException, InterruptedException, TimeoutException { + csrf = getCsrfToken(installationId); + logger.debug("Got CSRF: {}", csrf); + // Set installation + String url = SET_INSTALLATION + installationId; + httpClient.GET(url); + } + + public @Nullable String getCsrfToken(BigDecimal installationId) + throws ExecutionException, InterruptedException, TimeoutException { + String html = null; + String url = SETTINGS + installationId; + + ContentResponse resp = httpClient.GET(url); + html = resp.getContentAsString(); + logger.trace("url: {} html: {}", url, html); + + Document htmlDocument = Jsoup.parse(html); + Element nameInput = htmlDocument.select("input[name=_csrf]").first(); + if (nameInput != null) { + return nameInput.attr("value"); + } else { + return null; + } + } + + public @Nullable String getPinCode(BigDecimal installationId) { + return verisureInstallations.get(installationId).getPinCode(); + } + + private void setPasswordFromCookie() { + CookieStore c = httpClient.getCookieStore(); + List cookies = c.getCookies(); + cookies.forEach(cookie -> { + logger.trace("Response Cookie: {}", cookie); + if (cookie.getName().equals(PASSWORD_NAME)) { + password = cookie.getValue(); + logger.debug("Fetching vid {} from cookie", password); + } + }); + } + + private void logTraceWithPattern(int responseStatus, String content) { + if (logger.isTraceEnabled()) { + String pattern = "(?m)^\\s*\\r?\\n|\\r?\\n\\s*(?!.*\\r?\\n)"; + String replacement = ""; + logger.trace("HTTP Response ({}) Body:{}", responseStatus, content.replaceAll(pattern, replacement)); + } + } + + private boolean areWeLoggedIn() throws ExecutionException, InterruptedException, TimeoutException { + logger.debug("Checking if we are logged in"); + String url = STATUS; + + ContentResponse response = httpClient.newRequest(url).method(HttpMethod.GET).send(); + String content = response.getContentAsString(); + logTraceWithPattern(response.getStatus(), content); + + switch (response.getStatus()) { + case HttpStatus.OK_200: + if (content.contains("MyPages")) { + setPasswordFromCookie(); + return true; + } else { + logger.debug("Not on mypages,verisure.com, we need to login again!"); + return false; + } + case HttpStatus.MOVED_TEMPORARILY_302: + // Redirection + logger.debug("Status code 302. Redirected. Probably not logged in"); + return false; + case HttpStatus.INTERNAL_SERVER_ERROR_500: + case HttpStatus.SERVICE_UNAVAILABLE_503: + throw new HttpResponseException( + "Status code " + response.getStatus() + ". Verisure service temporarily down", response); + default: + logger.debug("Status code {} body {}", response.getStatus(), content); + break; + } + return false; + } + + private @Nullable T getJSONVerisureAPI(String url, Class jsonClass) + throws ExecutionException, InterruptedException, TimeoutException, JsonSyntaxException { + logger.debug("HTTP GET: {}", BASEURL + url); + + ContentResponse response = httpClient.GET(BASEURL + url + "?_=" + System.currentTimeMillis()); + String content = response.getContentAsString(); + logTraceWithPattern(response.getStatus(), content); + + return gson.fromJson(content, jsonClass); + } + + private ContentResponse postVerisureAPI(String url, String data, boolean isJSON) + throws ExecutionException, InterruptedException, TimeoutException { + logger.debug("postVerisureAPI URL: {} Data:{}", url, data); + Request request = httpClient.newRequest(url).method(HttpMethod.POST); + if (isJSON) { + request.header("content-type", "application/json"); + } else { + if (csrf != null) { + request.header("X-CSRF-TOKEN", csrf); + } + } + request.header("Accept", "application/json"); + if (!data.equals("empty")) { + request.content(new BytesContentProvider(data.getBytes(StandardCharsets.UTF_8)), + "application/x-www-form-urlencoded; charset=UTF-8"); + } else { + logger.debug("Setting cookie with username {} and vid {}", userName, password); + request.cookie(new HttpCookie(USER_NAME, userName)); + request.cookie(new HttpCookie(PASSWORD_NAME, password)); + } + logger.debug("HTTP POST Request {}.", request.toString()); + return request.send(); + } + + private T postJSONVerisureAPI(String url, String data, Class jsonClass) + throws ExecutionException, InterruptedException, TimeoutException, JsonSyntaxException, PostToAPIException { + for (int cnt = 0; cnt < APISERVERLIST.size(); cnt++) { + ContentResponse response = postVerisureAPI(apiServerInUse + url, data, Boolean.TRUE); + logger.debug("HTTP Response ({})", response.getStatus()); + if (response.getStatus() == HttpStatus.OK_200) { + String content = response.getContentAsString(); + if (content.contains("\"message\":\"Request Failed") && content.contains("503")) { + // Maybe Verisure has switched API server in use? + logger.debug("Changed API server! Response: {}", content); + setApiServerInUse(getNextApiServer()); + } else { + String contentChomped = content.trim(); + logger.trace("Response body: {}", content); + return gson.fromJson(contentChomped, jsonClass); + } + } else { + logger.debug("Failed to send POST, Http status code: {}", response.getStatus()); + } + } + throw new PostToAPIException("Failed to POST to API"); + } + + private int postVerisureAPI(String urlString, String data) { + String url; + if (urlString.contains("https://mypages")) { + url = urlString; + } else { + url = apiServerInUse + urlString; + } + + for (int cnt = 0; cnt < APISERVERLIST.size(); cnt++) { + try { + ContentResponse response = postVerisureAPI(url, data, Boolean.FALSE); + logger.debug("HTTP Response ({})", response.getStatus()); + int httpStatus = response.getStatus(); + if (httpStatus == HttpStatus.OK_200) { + String content = response.getContentAsString(); + if (content.contains("\"message\":\"Request Failed. Code 503 from")) { + if (url.contains("https://mypages")) { + // Not an API URL + return HttpStatus.SERVICE_UNAVAILABLE_503; + } else { + // Maybe Verisure has switched API server in use + setApiServerInUse(getNextApiServer()); + url = apiServerInUse + urlString; + } + } else { + logTraceWithPattern(httpStatus, content); + return httpStatus; + } + } else { + logger.debug("Failed to send POST, Http status code: {}", response.getStatus()); + } + } catch (ExecutionException | InterruptedException | TimeoutException e) { + logger.warn("Failed to send a POST to the API {}", e.getMessage()); + } + } + return HttpStatus.SERVICE_UNAVAILABLE_503; + } + + private int setSessionCookieAuthLogin() throws ExecutionException, InterruptedException, TimeoutException { + // URL to set status which will give us 2 cookies with username and password used for the session + String url = STATUS; + ContentResponse response = httpClient.GET(url); + logTraceWithPattern(response.getStatus(), response.getContentAsString()); + + url = AUTH_LOGIN; + return postVerisureAPI(url, "empty"); + } + + private boolean getInstallations() { + int httpResultCode = 0; + + try { + httpResultCode = setSessionCookieAuthLogin(); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + logger.warn("Failed to set session cookie {}", e.getMessage()); + return false; + } + + if (httpResultCode == HttpStatus.OK_200) { + String url = START_GRAPHQL; + + String queryQLAccountInstallations = "[{\"operationName\":\"AccountInstallations\",\"variables\":{\"email\":\"" + + userName + + "\"},\"query\":\"query AccountInstallations($email: String!) {\\n account(email: $email) {\\n owainstallations {\\n giid\\n alias\\n type\\n subsidiary\\n dealerId\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"; + try { + VerisureInstallationsDTO installations = postJSONVerisureAPI(url, queryQLAccountInstallations, + VerisureInstallationsDTO.class); + logger.debug("Installation: {}", installations.toString()); + List owaInstList = installations.getData().getAccount().getOwainstallations(); + boolean pinCodesMatchInstallations = true; + List pinCodes = null; + String pinCode = this.pinCode; + if (pinCode != null) { + pinCodes = Arrays.asList(pinCode.split(",")); + if (owaInstList.size() != pinCodes.size()) { + logger.debug("Number of installations {} does not match number of pin codes configured {}", + owaInstList.size(), pinCodes.size()); + pinCodesMatchInstallations = false; + } + } else { + logger.debug("No pin-code defined for user {}", userName); + } + + for (int i = 0; i < owaInstList.size(); i++) { + VerisureInstallation vInst = new VerisureInstallation(); + Owainstallation owaInstallation = owaInstList.get(i); + String installationId = owaInstallation.getGiid(); + if (owaInstallation.getAlias() != null && installationId != null) { + vInst.setInstallationId(new BigDecimal(installationId)); + vInst.setInstallationName(owaInstallation.getAlias()); + if (pinCode != null && pinCodes != null) { + int pinCodeIndex = pinCodesMatchInstallations ? i : 0; + vInst.setPinCode(pinCodes.get(pinCodeIndex)); + logger.debug("Setting configured pincode index[{}] to installation ID {}", pinCodeIndex, + installationId); + } + verisureInstallations.put(new BigDecimal(installationId), vInst); + } else { + logger.warn("Failed to get alias and/or giid"); + return false; + } + } + } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException + | PostToAPIException e) { + logger.warn("Failed to send a POST to the API {}", e.getMessage()); + } + } else { + logger.warn("Failed to set session cookie and auth login, HTTP result code: {}", httpResultCode); + return false; + } + return true; + } + + private synchronized boolean logIn() { + try { + if (!areWeLoggedIn()) { + logger.debug("Attempting to log in to mypages.verisure.com"); + String url = LOGON_SUF; + logger.debug("Login URL: {}", url); + int httpStatusCode = postVerisureAPI(url, authstring); + if (httpStatusCode != HttpStatus.OK_200) { + logger.debug("Failed to login, HTTP status code: {}", httpStatusCode); + return false; + } + return true; + } else { + return true; + } + } catch (ExecutionException | InterruptedException | TimeoutException e) { + logger.warn("Failed to login {}", e.getMessage()); + } + return false; + } + + private void notifyListeners(T thing) { + deviceStatusListeners.forEach(listener -> { + if (listener.getVerisureThingClass().equals(thing.getClass())) { + listener.onDeviceStateChanged(thing); + } + }); + } + + private void notifyListenersIfChanged(VerisureThingDTO thing, VerisureInstallation installation, String deviceId) { + String normalizedDeviceId = VerisureThingConfiguration.normalizeDeviceId(deviceId); + thing.setDeviceId(normalizedDeviceId); + thing.setSiteId(installation.getInstallationId()); + thing.setSiteName(installation.getInstallationName()); + VerisureThingDTO oldObj = verisureThings.get(normalizedDeviceId); + if (!thing.equals(oldObj)) { + verisureThings.put(thing.getDeviceId(), thing); + notifyListeners(thing); + } else { + logger.trace("No need to notify listeners for thing: {}", thing); + } + } + + private void updateStatus() { + logger.debug("Update status"); + verisureInstallations.forEach((installationId, installation) -> { + try { + configureInstallationInstance(installation.getInstallationId()); + int httpResultCode = setSessionCookieAuthLogin(); + if (httpResultCode == HttpStatus.OK_200) { + updateAlarmStatus(installation); + updateSmartLockStatus(installation); + updateMiceDetectionStatus(installation); + updateClimateStatus(installation); + updateDoorWindowStatus(installation); + updateUserPresenceStatus(installation); + updateSmartPlugStatus(installation); + updateBroadbandConnectionStatus(installation); + updateEventLogStatus(installation); + updateGatewayStatus(installation); + } else { + logger.debug("Failed to set session cookie and auth login, HTTP result code: {}", httpResultCode); + } + } catch (ExecutionException | InterruptedException | TimeoutException e) { + logger.debug("Failed to update status {}", e.getMessage()); + } + }); + } + + private String createOperationJSON(String operation, VariablesDTO variables, String query) { + OperationDTO operationJSON = new OperationDTO(); + operationJSON.setOperationName(operation); + operationJSON.setVariables(variables); + operationJSON.setQuery(query); + return gson.toJson(Collections.singletonList(operationJSON)); + } + + private synchronized void updateAlarmStatus(VerisureInstallation installation) { + BigDecimal installationId = installation.getInstallationId(); + String url = START_GRAPHQL; + String operation = "ArmState"; + VariablesDTO variables = new VariablesDTO(); + variables.setGiid(installationId.toString()); + String query = "query " + operation + + "($giid: String!) {\n installation(giid: $giid) {\n armState {\n type\n statusType\n date\n name\n changedVia\n allowedForFirstLine\n allowed\n errorCodes {\n value\n message\n __typename\n}\n __typename\n}\n __typename\n}\n}\n"; + + String queryQLAlarmStatus = createOperationJSON(operation, variables, query); + logger.debug("Quering API for alarm status!"); + try { + VerisureThingDTO thing = postJSONVerisureAPI(url, queryQLAlarmStatus, VerisureAlarmsDTO.class); + logger.debug("REST Response ({})", thing); + // Set unique deviceID + String deviceId = "alarm" + installationId; + thing.setDeviceId(deviceId); + notifyListenersIfChanged(thing, installation, deviceId); + } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException + | PostToAPIException e) { + logger.warn("Failed to send a POST to the API {}", e.getMessage()); + } + } + + private synchronized void updateSmartLockStatus(VerisureInstallation installation) { + BigDecimal installationId = installation.getInstallationId(); + String url = START_GRAPHQL; + String operation = "DoorLock"; + String query = "query " + operation + + "($giid: String!) {\n installation(giid: $giid) {\n doorlocks {\n device {\n deviceLabel\n area\n __typename\n}\n currentLockState\n eventTime\n secureModeActive\n motorJam\n userString\n method\n __typename\n}\n __typename\n}\n}\n"; + VariablesDTO variables = new VariablesDTO(); + variables.setGiid(installationId.toString()); + String queryQLSmartLock = createOperationJSON(operation, variables, query); + logger.debug("Quering API for smart lock status"); + + try { + VerisureSmartLocksDTO thing = postJSONVerisureAPI(url, queryQLSmartLock, VerisureSmartLocksDTO.class); + logger.debug("REST Response ({})", thing); + List doorLockList = thing.getData().getInstallation().getDoorlocks(); + doorLockList.forEach(doorLock -> { + VerisureSmartLocksDTO slThing = new VerisureSmartLocksDTO(); + VerisureSmartLocksDTO.Installation inst = new VerisureSmartLocksDTO.Installation(); + inst.setDoorlocks(Collections.singletonList(doorLock)); + VerisureSmartLocksDTO.Data data = new VerisureSmartLocksDTO.Data(); + data.setInstallation(inst); + slThing.setData(data); + // Set unique deviceID + String deviceId = doorLock.getDevice().getDeviceLabel(); + if (deviceId != null) { + // Set location + slThing.setLocation(doorLock.getDevice().getArea()); + slThing.setDeviceId(deviceId); + // Fetch more info from old endpoint + try { + VerisureSmartLockDTO smartLockThing = getJSONVerisureAPI(SMARTLOCK_PATH + slThing.getDeviceId(), + VerisureSmartLockDTO.class); + logger.debug("REST Response ({})", smartLockThing); + slThing.setSmartLockJSON(smartLockThing); + notifyListenersIfChanged(slThing, installation, deviceId); + } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException e) { + logger.warn("Failed to query for smartlock status: {}", e.getMessage()); + } + } + }); + + } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException + | PostToAPIException e) { + logger.warn("Failed to send a POST to the API {}", e.getMessage()); + } + } + + private synchronized void updateSmartPlugStatus(VerisureInstallation installation) { + BigDecimal installationId = installation.getInstallationId(); + String url = START_GRAPHQL; + String operation = "SmartPlug"; + VariablesDTO variables = new VariablesDTO(); + variables.setGiid(installationId.toString()); + String query = "query " + operation + + "($giid: String!) {\n installation(giid: $giid) {\n smartplugs {\n device {\n deviceLabel\n area\n gui {\n support\n label\n __typename\n}\n __typename\n}\n currentState\n icon\n isHazardous\n __typename\n}\n __typename\n}\n}\n"; + String queryQLSmartPlug = createOperationJSON(operation, variables, query); + logger.debug("Quering API for smart plug status"); + + try { + VerisureSmartPlugsDTO thing = postJSONVerisureAPI(url, queryQLSmartPlug, VerisureSmartPlugsDTO.class); + logger.debug("REST Response ({})", thing); + List smartPlugList = thing.getData().getInstallation().getSmartplugs(); + smartPlugList.forEach(smartPlug -> { + VerisureSmartPlugsDTO spThing = new VerisureSmartPlugsDTO(); + VerisureSmartPlugsDTO.Installation inst = new VerisureSmartPlugsDTO.Installation(); + inst.setSmartplugs(Collections.singletonList(smartPlug)); + VerisureSmartPlugsDTO.Data data = new VerisureSmartPlugsDTO.Data(); + data.setInstallation(inst); + spThing.setData(data); + // Set unique deviceID + String deviceId = smartPlug.getDevice().getDeviceLabel(); + if (deviceId != null) { + // Set location + spThing.setLocation(smartPlug.getDevice().getArea()); + notifyListenersIfChanged(spThing, installation, deviceId); + } + }); + } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException + | PostToAPIException e) { + logger.warn("Failed to send a POST to the API {}", e.getMessage()); + } + } + + private synchronized void updateClimateStatus(VerisureInstallation installation) { + BigDecimal installationId = installation.getInstallationId(); + String url = START_GRAPHQL; + VariablesDTO variables = new VariablesDTO(); + variables.setGiid(installationId.toString()); + String operation = "Climate"; + String query = "query " + operation + + "($giid: String!) {\n installation(giid: $giid) {\n climates {\n device {\n deviceLabel\n area\n gui {\n label\n __typename\n }\n __typename\n }\n humidityEnabled\n humidityTimestamp\n humidityValue\n temperatureTimestamp\n temperatureValue\n __typename\n }\n __typename\n}\n}\n"; + + String queryQLClimates = createOperationJSON(operation, variables, query); + logger.debug("Quering API for climate status"); + + try { + VerisureClimatesDTO thing = postJSONVerisureAPI(url, queryQLClimates, VerisureClimatesDTO.class); + logger.debug("REST Response ({})", thing); + List climateList = thing.getData().getInstallation().getClimates(); + climateList.forEach(climate -> { + // If thing is Mouse detection device, then skip it, but fetch temperature from it + String type = climate.getDevice().getGui().getLabel(); + if ("MOUSE".equals(type)) { + logger.debug("Mouse detection device!"); + String deviceId = climate.getDevice().getDeviceLabel(); + if (deviceId != null) { + deviceId = VerisureThingConfiguration.normalizeDeviceId(deviceId); + VerisureThingDTO mouseThing = verisureThings.get(deviceId); + if (mouseThing != null && mouseThing instanceof VerisureMiceDetectionDTO) { + VerisureMiceDetectionDTO miceDetectorThing = (VerisureMiceDetectionDTO) mouseThing; + miceDetectorThing.setTemperatureValue(climate.getTemperatureValue()); + miceDetectorThing.setTemperatureTime(climate.getTemperatureTimestamp()); + notifyListeners(miceDetectorThing); + logger.debug("Found climate thing for a Verisure Mouse Detector"); + } + } + return; + } + VerisureClimatesDTO cThing = new VerisureClimatesDTO(); + VerisureClimatesDTO.Installation inst = new VerisureClimatesDTO.Installation(); + inst.setClimates(Collections.singletonList(climate)); + VerisureClimatesDTO.Data data = new VerisureClimatesDTO.Data(); + data.setInstallation(inst); + cThing.setData(data); + // Set unique deviceID + String deviceId = climate.getDevice().getDeviceLabel(); + if (deviceId != null) { + // Set location + cThing.setLocation(climate.getDevice().getArea()); + notifyListenersIfChanged(cThing, installation, deviceId); + } + }); + } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException + | PostToAPIException e) { + logger.warn("Failed to send a POST to the API {}", e.getMessage()); + } + } + + private synchronized void updateDoorWindowStatus(VerisureInstallation installation) { + BigDecimal installationId = installation.getInstallationId(); + String url = START_GRAPHQL; + String operation = "DoorWindow"; + VariablesDTO variables = new VariablesDTO(); + variables.setGiid(installationId.toString()); + String query = "query " + operation + + "($giid: String!) {\n installation(giid: $giid) {\n doorWindows {\n device {\n deviceLabel\n area\n __typename\n }\n type\n state\n wired\n reportTime\n __typename\n }\n __typename\n}\n}\n"; + + String queryQLDoorWindow = createOperationJSON(operation, variables, query); + logger.debug("Quering API for door&window status"); + + try { + VerisureDoorWindowsDTO thing = postJSONVerisureAPI(url, queryQLDoorWindow, VerisureDoorWindowsDTO.class); + logger.debug("REST Response ({})", thing); + List doorWindowList = thing.getData().getInstallation().getDoorWindows(); + doorWindowList.forEach(doorWindow -> { + VerisureDoorWindowsDTO dThing = new VerisureDoorWindowsDTO(); + VerisureDoorWindowsDTO.Installation inst = new VerisureDoorWindowsDTO.Installation(); + inst.setDoorWindows(Collections.singletonList(doorWindow)); + VerisureDoorWindowsDTO.Data data = new VerisureDoorWindowsDTO.Data(); + data.setInstallation(inst); + dThing.setData(data); + // Set unique deviceID + String deviceId = doorWindow.getDevice().getDeviceLabel(); + if (deviceId != null) { + // Set location + dThing.setLocation(doorWindow.getDevice().getArea()); + notifyListenersIfChanged(dThing, installation, deviceId); + } + }); + } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException + | PostToAPIException e) { + logger.warn("Failed to send a POST to the API {}", e.getMessage()); + } + } + + private synchronized void updateBroadbandConnectionStatus(VerisureInstallation inst) { + BigDecimal installationId = inst.getInstallationId(); + String url = START_GRAPHQL; + String operation = "Broadband"; + VariablesDTO variables = new VariablesDTO(); + variables.setGiid(installationId.toString()); + String query = "query " + operation + + "($giid: String!) {\n installation(giid: $giid) {\n broadband {\n testDate\n isBroadbandConnected\n __typename\n }\n __typename\n}\n}\n"; + + String queryQLBroadbandConnection = createOperationJSON(operation, variables, query); + logger.debug("Quering API for broadband connection status"); + + try { + VerisureThingDTO thing = postJSONVerisureAPI(url, queryQLBroadbandConnection, + VerisureBroadbandConnectionsDTO.class); + logger.debug("REST Response ({})", thing); + // Set unique deviceID + String deviceId = "bc" + installationId; + notifyListenersIfChanged(thing, inst, deviceId); + } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException + | PostToAPIException e) { + logger.warn("Failed to send a POST to the API {}", e.getMessage()); + } + } + + private synchronized void updateUserPresenceStatus(VerisureInstallation installation) { + BigDecimal installationId = installation.getInstallationId(); + String url = START_GRAPHQL; + String operation = "userTrackings"; + VariablesDTO variables = new VariablesDTO(); + variables.setGiid(installationId.toString()); + String query = "query " + operation + + "($giid: String!) {\ninstallation(giid: $giid) {\n userTrackings {\n isCallingUser\n webAccount\n status\n xbnContactId\n currentLocationName\n deviceId\n name\n currentLocationTimestamp\n deviceName\n currentLocationId\n __typename\n}\n __typename\n}\n}\n"; + + String queryQLUserPresence = createOperationJSON(operation, variables, query); + logger.debug("Quering API for user presence status"); + + try { + VerisureUserPresencesDTO thing = postJSONVerisureAPI(url, queryQLUserPresence, + VerisureUserPresencesDTO.class); + logger.debug("REST Response ({})", thing); + List userTrackingList = thing.getData().getInstallation() + .getUserTrackings(); + userTrackingList.forEach(userTracking -> { + String localUserTrackingStatus = userTracking.getStatus(); + if (localUserTrackingStatus != null && localUserTrackingStatus.equals("ACTIVE")) { + VerisureUserPresencesDTO upThing = new VerisureUserPresencesDTO(); + VerisureUserPresencesDTO.Installation inst = new VerisureUserPresencesDTO.Installation(); + inst.setUserTrackings(Collections.singletonList(userTracking)); + VerisureUserPresencesDTO.Data data = new VerisureUserPresencesDTO.Data(); + data.setInstallation(inst); + upThing.setData(data); + // Set unique deviceID + String deviceId = "up" + userTracking.getWebAccount() + installationId; + notifyListenersIfChanged(upThing, installation, deviceId); + } + }); + } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException + | PostToAPIException e) { + logger.warn("Failed to send a POST to the API {}", e.getMessage()); + } + } + + private synchronized void updateMiceDetectionStatus(VerisureInstallation installation) { + BigDecimal installationId = installation.getInstallationId(); + String url = START_GRAPHQL; + String operation = "Mouse"; + VariablesDTO variables = new VariablesDTO(); + variables.setGiid(installationId.toString()); + String query = "query " + operation + + "($giid: String!) {\n installation(giid: $giid) {\n mice {\n device {\n deviceLabel\n area\n gui {\n support\n __typename\n}\n __typename\n}\n type\n detections {\n count\n gatewayTime\n nodeTime\n duration\n __typename\n}\n __typename\n}\n __typename\n}\n}\n"; + + String queryQLMiceDetection = createOperationJSON(operation, variables, query); + logger.debug("Quering API for mice detection status"); + + try { + VerisureMiceDetectionDTO thing = postJSONVerisureAPI(url, queryQLMiceDetection, + VerisureMiceDetectionDTO.class); + logger.debug("REST Response ({})", thing); + List miceList = thing.getData().getInstallation().getMice(); + miceList.forEach(mouse -> { + VerisureMiceDetectionDTO miceThing = new VerisureMiceDetectionDTO(); + VerisureMiceDetectionDTO.Installation inst = new VerisureMiceDetectionDTO.Installation(); + inst.setMice(Collections.singletonList(mouse)); + VerisureMiceDetectionDTO.Data data = new VerisureMiceDetectionDTO.Data(); + data.setInstallation(inst); + miceThing.setData(data); + // Set unique deviceID + String deviceId = mouse.getDevice().getDeviceLabel(); + logger.debug("Mouse id: {} for thing: {}", deviceId, mouse); + if (deviceId != null) { + // Set location + miceThing.setLocation(mouse.getDevice().getArea()); + notifyListenersIfChanged(miceThing, installation, deviceId); + } + }); + } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException + | PostToAPIException e) { + logger.warn("Failed to send a POST to the API {}", e.getMessage()); + } + } + + private synchronized void updateEventLogStatus(VerisureInstallation installation) { + BigDecimal installationId = installation.getInstallationId(); + String url = START_GRAPHQL; + String operation = "EventLog"; + int offset = 0; + int numberOfEvents = this.numberOfEvents; + List eventCategories = new ArrayList<>(Arrays.asList("INTRUSION", "FIRE", "SOS", "WATER", "ANIMAL", + "TECHNICAL", "WARNING", "ARM", "DISARM", "LOCK", "UNLOCK", "PICTURE", "CLIMATE", "CAMERA_SETTINGS", + "DOORWINDOW_STATE_OPENED", "DOORWINDOW_STATE_CLOSED", "USERTRACKING")); + VariablesDTO variables = new VariablesDTO(); + variables.setGiid(installationId.toString()); + variables.setHideNotifications(true); + variables.setOffset(offset); + variables.setPagesize(numberOfEvents); + variables.setEventCategories(eventCategories); + String query = "query " + operation + + "($giid: String!, $offset: Int!, $pagesize: Int!, $eventCategories: [String], $fromDate: String, $toDate: String, $eventContactIds: [String]) {\n installation(giid: $giid) {\n eventLog(offset: $offset, pagesize: $pagesize, eventCategories: $eventCategories, eventContactIds: $eventContactIds, fromDate: $fromDate, toDate: $toDate) {\n moreDataAvailable\n pagedList {\n device {\n deviceLabel\n area\n gui {\n label\n __typename\n }\n __typename\n }\n gatewayArea\n eventType\n eventCategory\n eventSource\n eventId\n eventTime\n userName\n armState\n userType\n climateValue\n sensorType\n eventCount\n __typename\n }\n __typename\n }\n __typename\n }\n}\n"; + + String queryQLEventLog = createOperationJSON(operation, variables, query); + logger.debug("Quering API for event log status"); + + try { + VerisureEventLogDTO thing = postJSONVerisureAPI(url, queryQLEventLog, VerisureEventLogDTO.class); + logger.debug("REST Response ({})", thing); + // Set unique deviceID + String deviceId = "el" + installationId; + notifyListenersIfChanged(thing, installation, deviceId); + } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException + | PostToAPIException e) { + logger.warn("Failed to send a POST to the API {}", e.getMessage()); + } + } + + private synchronized void updateGatewayStatus(VerisureInstallation installation) { + BigDecimal installationId = installation.getInstallationId(); + String url = START_GRAPHQL; + String operation = "communicationState"; + VariablesDTO variables = new VariablesDTO(); + variables.setGiid(installationId.toString()); + + String query = "query " + operation + + "($giid: String!) {\n installation(giid: $giid) {\n communicationState {\n hardwareCarrierType\n result\n mediaType\n device {\n deviceLabel\n area\n gui {\n label\n __typename\n }\n __typename\n }\n testDate\n __typename\n }\n __typename\n }\n}"; + + String queryQLEventLog = createOperationJSON(operation, variables, query); + logger.debug("Quering API for gateway status"); + + try { + VerisureGatewayDTO thing = postJSONVerisureAPI(url, queryQLEventLog, VerisureGatewayDTO.class); + logger.debug("REST Response ({})", thing); + // Set unique deviceID + List communicationStateList = thing.getData().getInstallation().getCommunicationState(); + if (!communicationStateList.isEmpty()) { + String deviceId = communicationStateList.get(0).getDevice().getDeviceLabel(); + if (deviceId != null) { + notifyListenersIfChanged(thing, installation, deviceId); + } + } + } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException + | PostToAPIException e) { + logger.warn("Failed to send a POST to the API {}", e.getMessage()); + } + } + + private final class VerisureInstallation { + private @Nullable String installationName; + private BigDecimal installationId = BigDecimal.ZERO; + private @Nullable String pinCode; + + public @Nullable String getPinCode() { + return pinCode; + } + + public void setPinCode(@Nullable String pinCode) { + this.pinCode = pinCode; + } + + public VerisureInstallation() { + } + + public BigDecimal getInstallationId() { + return installationId; + } + + public @Nullable String getInstallationName() { + return installationName; + } + + public void setInstallationId(BigDecimal installationId) { + this.installationId = installationId; + } + + public void setInstallationName(@Nullable String installationName) { + this.installationName = installationName; + } + } + + private static class OperationDTO { + + @SuppressWarnings("unused") + private @Nullable String operationName; + @SuppressWarnings("unused") + private VariablesDTO variables = new VariablesDTO(); + @SuppressWarnings("unused") + private @Nullable String query; + + public void setOperationName(String operationName) { + this.operationName = operationName; + } + + public void setVariables(VariablesDTO variables) { + this.variables = variables; + } + + public void setQuery(String query) { + this.query = query; + } + } + + public static class VariablesDTO { + + @SuppressWarnings("unused") + private boolean hideNotifications; + @SuppressWarnings("unused") + private int offset; + @SuppressWarnings("unused") + private int pagesize; + @SuppressWarnings("unused") + private @Nullable List eventCategories = null; + @SuppressWarnings("unused") + private @Nullable String giid; + + public void setHideNotifications(boolean hideNotifications) { + this.hideNotifications = hideNotifications; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public void setPagesize(int pagesize) { + this.pagesize = pagesize; + } + + public void setEventCategories(List eventCategories) { + this.eventCategories = eventCategories; + } + + public void setGiid(String giid) { + this.giid = giid; + } + } + + private class PostToAPIException extends Exception { + + private static final long serialVersionUID = 1L; + + public PostToAPIException(String message) { + super(message); + } + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureThingConfiguration.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureThingConfiguration.java new file mode 100644 index 0000000000000..64c537cecb009 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureThingConfiguration.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Configuration class for VerisureThingHandler. + * + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public class VerisureThingConfiguration { + public static final String DEVICE_ID_LABEL = "deviceId"; + + private String deviceId = ""; + private int numberOfEvents; + private int eventTriggerDelay; + + public String getDeviceId() { + // Make sure device id is normalized, i.e. replace all non character/digits with empty string + return normalizeDeviceId(deviceId); + } + + public static String normalizeDeviceId(String unnormalizedDeviceId) { + return unnormalizedDeviceId.replaceAll("[^a-zA-Z0-9]+", ""); + } + + public int getNumberOfEvents() { + return numberOfEvents; + } + + public int getEventTriggerDelay() { + return eventTriggerDelay; + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/discovery/VerisureThingDiscoveryService.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/discovery/VerisureThingDiscoveryService.java new file mode 100644 index 0000000000000..4a02e79cd4400 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/discovery/VerisureThingDiscoveryService.java @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.discovery; + +import java.util.Collection; +import java.util.Collections; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; +import org.eclipse.smarthome.config.discovery.DiscoveryResult; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.openhab.binding.verisure.internal.VerisureHandlerFactory; +import org.openhab.binding.verisure.internal.VerisureSession; +import org.openhab.binding.verisure.internal.VerisureThingConfiguration; +import org.openhab.binding.verisure.internal.dto.VerisureThingDTO; +import org.openhab.binding.verisure.internal.handler.VerisureBridgeHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The discovery service, notified by a listener on the VerisureSession. + * + * @author Jarle Hjortland - Initial contribution + * @author Jan Gustafsson - Further development + * + */ +@NonNullByDefault +public class VerisureThingDiscoveryService extends AbstractDiscoveryService + implements DiscoveryService, ThingHandlerService { + + private static final int SEARCH_TIME_SECONDS = 60; + private final Logger logger = LoggerFactory.getLogger(VerisureThingDiscoveryService.class); + + private @NonNullByDefault({}) VerisureBridgeHandler verisureBridgeHandler; + private @NonNullByDefault({}) ThingUID bridgeUID; + + public VerisureThingDiscoveryService() { + super(VerisureHandlerFactory.SUPPORTED_THING_TYPES, SEARCH_TIME_SECONDS); + } + + @Override + public void startScan() { + logger.debug("VerisureThingDiscoveryService:startScan"); + removeOlderResults(getTimestampOfLastScan()); + if (verisureBridgeHandler != null) { + VerisureSession session = verisureBridgeHandler.getSession(); + if (session != null) { + Collection verisureThings = session.getVerisureThings(); + verisureThings.stream().forEach(thing -> { + logger.debug("Discovered thing: {}", thing); + onThingAddedInternal(thing); + }); + } + } + } + + private void onThingAddedInternal(VerisureThingDTO thing) { + logger.debug("VerisureThingDiscoveryService:OnThingAddedInternal"); + ThingUID thingUID = getThingUID(thing); + String deviceId = thing.getDeviceId(); + if (thingUID != null) { + if (verisureBridgeHandler != null) { + String label = "Device Id: " + deviceId; + if (thing.getLocation() != null) { + label += ", Location: " + thing.getLocation(); + } + if (thing.getSiteName() != null) { + label += ", Site name: " + thing.getSiteName(); + } + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID) + .withLabel(label).withProperty(VerisureThingConfiguration.DEVICE_ID_LABEL, deviceId) + .withRepresentationProperty(deviceId).build(); + logger.debug("thinguid: {}, bridge {}, label {}", thingUID, bridgeUID, deviceId); + thingDiscovered(discoveryResult); + } + } else { + logger.debug("Discovered unsupported thing of type '{}' with deviceId {}", thing.getClass(), deviceId); + } + } + + private @Nullable ThingUID getThingUID(VerisureThingDTO thing) { + ThingUID thingUID = null; + if (verisureBridgeHandler != null) { + String deviceId = thing.getDeviceId(); + // Make sure device id is normalized, i.e. replace all non character/digits with empty string + deviceId = VerisureThingConfiguration.normalizeDeviceId(deviceId); + thingUID = new ThingUID(thing.getThingTypeUID(), bridgeUID, deviceId); + } + return thingUID; + } + + @Override + public void activate() { + super.activate(Collections.singletonMap(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY, true)); + } + + @Override + public void deactivate() { + super.deactivate(); + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof VerisureBridgeHandler) { + verisureBridgeHandler = (VerisureBridgeHandler) handler; + bridgeUID = verisureBridgeHandler.getUID(); + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return verisureBridgeHandler; + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureAlarmsDTO.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureAlarmsDTO.java new file mode 100644 index 0000000000000..7d6ab0a659920 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureAlarmsDTO.java @@ -0,0 +1,213 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.dto; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.THING_TYPE_ALARM; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The alarms of the Verisure System. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureAlarmsDTO extends VerisureBaseThingDTO { + + @Override + public ThingTypeUID getThingTypeUID() { + return THING_TYPE_ALARM; + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + return true; + } + + public static class ArmState { + + private @Nullable String type; + private @Nullable String statusType; + private @Nullable String date; + private @Nullable String name; + private @Nullable String changedVia; + private boolean allowedForFirstLine; + private boolean allowed; + private List errorCodes = new ArrayList<>(); + private @Nullable String typename; + + public @Nullable String getType() { + return type; + } + + public @Nullable String getStatusType() { + return statusType; + } + + public void setStatusType(@Nullable String statusType) { + this.statusType = statusType; + } + + public @Nullable String getDate() { + return date; + } + + public @Nullable String getName() { + return name; + } + + public @Nullable String getChangedVia() { + return changedVia; + } + + public boolean getAllowedForFirstLine() { + return allowedForFirstLine; + } + + public boolean getAllowed() { + return allowed; + } + + public List getErrorCodes() { + return errorCodes; + } + + public @Nullable String getTypename() { + return typename; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (allowed ? 1231 : 1237); + result = prime * result + (allowedForFirstLine ? 1231 : 1237); + String localChangedVia = changedVia; + result = prime * result + ((localChangedVia == null) ? 0 : localChangedVia.hashCode()); + String localDate = date; + result = prime * result + ((localDate == null) ? 0 : localDate.hashCode()); + result = prime * result + errorCodes.hashCode(); + String localName = name; + result = prime * result + ((localName == null) ? 0 : localName.hashCode()); + String localStatusType = statusType; + result = prime * result + ((localStatusType == null) ? 0 : localStatusType.hashCode()); + String localType = type; + result = prime * result + ((localType == null) ? 0 : localType.hashCode()); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ArmState other = (ArmState) obj; + if (allowed != other.allowed) { + return false; + } + if (allowedForFirstLine != other.allowedForFirstLine) { + return false; + } + String localChangedVia = changedVia; + if (localChangedVia == null) { + if (other.changedVia != null) { + return false; + } + } else if (!localChangedVia.equals(other.changedVia)) { + return false; + } + String localdate = date; + if (localdate == null) { + if (other.date != null) { + return false; + } + } else if (!localdate.equals(other.date)) { + return false; + } + if (!errorCodes.equals(other.errorCodes)) { + return false; + } + String localName = name; + if (localName == null) { + if (other.name != null) { + return false; + } + } else if (!localName.equals(other.name)) { + return false; + } + String localStatusType = statusType; + if (localStatusType == null) { + if (other.statusType != null) { + return false; + } + } else if (!localStatusType.equals(other.statusType)) { + return false; + } + String localType = type; + if (localType == null) { + if (other.type != null) { + return false; + } + } else if (!localType.equals(other.type)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "ArmState [type=" + type + ", statusType=" + statusType + ", date=" + date + ", name=" + name + + ", changedVia=" + changedVia + ", allowedForFirstLine=" + allowedForFirstLine + ", allowed=" + + allowed + ", errorCodes=" + errorCodes + ", typename=" + typename + "]"; + } + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureBaseThingDTO.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureBaseThingDTO.java new file mode 100644 index 0000000000000..88df7284ae3d9 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureBaseThingDTO.java @@ -0,0 +1,571 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.dto; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.openhab.binding.verisure.internal.VerisureThingConfiguration; +import org.openhab.binding.verisure.internal.dto.VerisureAlarmsDTO.ArmState; +import org.openhab.binding.verisure.internal.dto.VerisureBroadbandConnectionsDTO.Broadband; +import org.openhab.binding.verisure.internal.dto.VerisureClimatesDTO.Climate; +import org.openhab.binding.verisure.internal.dto.VerisureDoorWindowsDTO.DoorWindow; +import org.openhab.binding.verisure.internal.dto.VerisureEventLogDTO.EventLog; +import org.openhab.binding.verisure.internal.dto.VerisureGatewayDTO.CommunicationState; +import org.openhab.binding.verisure.internal.dto.VerisureMiceDetectionDTO.Mouse; +import org.openhab.binding.verisure.internal.dto.VerisureSmartLocksDTO.Doorlock; +import org.openhab.binding.verisure.internal.dto.VerisureSmartPlugsDTO.Smartplug; +import org.openhab.binding.verisure.internal.dto.VerisureUserPresencesDTO.UserTracking; + +import com.google.gson.annotations.SerializedName; + +/** + * A base JSON thing for other Verisure things to inherit from. + * + * @author Jarle Hjortland - Initial contribution + * + */ +@NonNullByDefault +public abstract class VerisureBaseThingDTO implements VerisureThingDTO { + + protected String deviceId = ""; + protected @Nullable String name; + protected @Nullable String location; + protected @Nullable String status; + protected @Nullable String siteName; + protected BigDecimal siteId = BigDecimal.ZERO; + protected Data data = new Data(); + + public @Nullable String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public @Nullable String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getDeviceId() { + return deviceId; + } + + @Override + public void setDeviceId(String deviceId) { + // Make sure device id is normalized + this.deviceId = VerisureThingConfiguration.normalizeDeviceId(deviceId); + } + + @Override + public @Nullable String getLocation() { + return location; + } + + public void setLocation(@Nullable String location) { + this.location = location; + } + + @Override + public @Nullable String getSiteName() { + return siteName; + } + + @Override + public void setSiteName(@Nullable String siteName) { + this.siteName = siteName; + } + + @Override + public BigDecimal getSiteId() { + return siteId; + } + + @Override + public void setSiteId(BigDecimal siteId) { + this.siteId = siteId; + } + + @Override + public abstract ThingTypeUID getThingTypeUID(); + + public Data getData() { + return data; + } + + public void setData(Data data) { + this.data = data; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + data.hashCode(); + result = prime * result + deviceId.hashCode(); + String localLocation = location; + result = prime * result + ((localLocation == null) ? 0 : localLocation.hashCode()); + String localName = name; + result = prime * result + ((localName == null) ? 0 : localName.hashCode()); + result = prime * result + siteId.hashCode(); + String localSiteName = siteName; + result = prime * result + ((localSiteName == null) ? 0 : localSiteName.hashCode()); + String localStatus = status; + result = prime * result + ((localStatus == null) ? 0 : localStatus.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + VerisureBaseThingDTO other = (VerisureBaseThingDTO) obj; + if (!data.equals(other.data)) { + return false; + } + if (!deviceId.equals(other.deviceId)) { + return false; + } + String localLocation = location; + if (localLocation == null) { + if (other.location != null) { + return false; + } + } else if (!localLocation.equals(other.location)) { + return false; + } + String localName = name; + if (localName == null) { + if (other.name != null) { + return false; + } + } else if (!localName.equals(other.name)) { + return false; + } + if (!siteId.equals(other.siteId)) { + return false; + } + String localSiteName = siteName; + if (localSiteName == null) { + if (other.siteName != null) { + return false; + } + } else if (!localSiteName.equals(other.siteName)) { + return false; + } + String localStatus = status; + if (localStatus == null) { + if (other.status != null) { + return false; + } + } else if (!localStatus.equals(other.status)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "VerisureBaseThingDTO [deviceId=" + deviceId + ", name=" + name + ", location=" + location + ", status=" + + status + ", siteName=" + siteName + ", siteId=" + siteId + ", data=" + data + "]"; + } + + public static class Data { + private Installation installation = new Installation(); + + public Installation getInstallation() { + return installation; + } + + public void setInstallation(Installation installation) { + this.installation = installation; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + installation.hashCode(); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Data other = (Data) obj; + if (!installation.equals(other.installation)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Data [installation=" + installation + "]"; + } + } + + public static class Installation { + + private ArmState armState = new ArmState(); + private Broadband broadband = new Broadband(); + private EventLog eventLog = new EventLog(); + private List climates = new ArrayList<>(); + private List doorWindows = new ArrayList<>(); + private List communicationState = new ArrayList<>(); + private List mice = new ArrayList<>(); + private List doorlocks = new ArrayList<>(); + private List smartplugs = new ArrayList<>(); + private List userTrackings = new ArrayList<>(); + + @SerializedName("__typename") + private @Nullable String typename; + + public ArmState getArmState() { + return armState; + } + + public Broadband getBroadband() { + return broadband; + } + + public List getClimates() { + return climates; + } + + public void setClimates(List climates) { + this.climates = climates; + } + + public List getDoorWindows() { + return doorWindows; + } + + public void setDoorWindows(List doorWindows) { + this.doorWindows = doorWindows; + } + + public EventLog getEventLog() { + return eventLog; + } + + public List getCommunicationState() { + return communicationState; + } + + public List getMice() { + return mice; + } + + public void setMice(List mice) { + this.mice = mice; + } + + public List getDoorlocks() { + return doorlocks; + } + + public void setDoorlocks(List doorlocks) { + this.doorlocks = doorlocks; + } + + public List getSmartplugs() { + return smartplugs; + } + + public void setSmartplugs(List smartplugs) { + this.smartplugs = smartplugs; + } + + public List getUserTrackings() { + return userTrackings; + } + + public void setUserTrackings(List userTrackings) { + this.userTrackings = userTrackings; + } + + public @Nullable String getTypename() { + return typename; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + armState.hashCode(); + result = prime * result + broadband.hashCode(); + result = prime * result + climates.hashCode(); + result = prime * result + communicationState.hashCode(); + result = prime * result + doorWindows.hashCode(); + result = prime * result + doorlocks.hashCode(); + result = prime * result + eventLog.hashCode(); + result = prime * result + mice.hashCode(); + result = prime * result + smartplugs.hashCode(); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + result = prime * result + userTrackings.hashCode(); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Installation other = (Installation) obj; + if (!armState.equals(other.armState)) { + return false; + } + if (!broadband.equals(other.broadband)) { + return false; + } + if (!climates.equals(other.climates)) { + return false; + } + if (!communicationState.equals(other.communicationState)) { + return false; + } + if (!doorWindows.equals(other.doorWindows)) { + return false; + } + if (!doorlocks.equals(other.doorlocks)) { + return false; + } + if (!eventLog.equals(other.eventLog)) { + return false; + } + if (!mice.equals(other.mice)) { + return false; + } + if (!smartplugs.equals(other.smartplugs)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + if (!userTrackings.equals(other.userTrackings)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Installation [armState=" + armState + ", broadband=" + broadband + ", eventLog=" + eventLog + + ", climates=" + climates + ", doorWindows=" + doorWindows + ", communicationState=" + + communicationState + ", mice=" + mice + ", doorlocks=" + doorlocks + ", smartplugs=" + smartplugs + + ", userTrackings=" + userTrackings + ", typename=" + typename + "]"; + } + } + + public static class Device { + + private @Nullable String deviceLabel; + private @Nullable String area; + private Gui gui = new Gui(); + @SerializedName("__typename") + private @Nullable String typename; + + public @Nullable String getDeviceLabel() { + return deviceLabel; + } + + public @Nullable String getArea() { + return area; + } + + public Gui getGui() { + return gui; + } + + public @Nullable String getTypename() { + return typename; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + String localArea = area; + result = prime * result + ((localArea == null) ? 0 : localArea.hashCode()); + String localDeviceLabel = deviceLabel; + result = prime * result + ((localDeviceLabel == null) ? 0 : localDeviceLabel.hashCode()); + result = prime * result + gui.hashCode(); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Device other = (Device) obj; + String localArea = area; + if (localArea == null) { + if (other.area != null) { + return false; + } + } else if (!localArea.equals(other.area)) { + return false; + } + String localDeviceLabel = deviceLabel; + if (localDeviceLabel == null) { + if (other.deviceLabel != null) { + return false; + } + } else if (!localDeviceLabel.equals(other.deviceLabel)) { + return false; + } + if (!gui.equals(other.gui)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Device [deviceLabel=" + deviceLabel + ", area=" + area + ", gui=" + gui + ", typename=" + typename + + "]"; + } + } + + public static class Gui { + + private @Nullable String label; + private @Nullable String support; + @SerializedName("__typename") + private @Nullable String typename; + + public @Nullable String getLabel() { + return label; + } + + public @Nullable String getSupport() { + return support; + } + + public @Nullable String getTypename() { + return typename; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + String localLabel = label; + result = prime * result + ((localLabel == null) ? 0 : localLabel.hashCode()); + String localSupport = support; + result = prime * result + ((localSupport == null) ? 0 : localSupport.hashCode()); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Gui other = (Gui) obj; + String localLabel = label; + if (localLabel == null) { + if (other.label != null) { + return false; + } + } else if (!localLabel.equals(other.label)) { + return false; + } + String localSupport = support; + if (localSupport == null) { + if (other.support != null) { + return false; + } + } else if (!localSupport.equals(other.support)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Gui [label=" + label + ", support=" + support + ", typename=" + typename + "]"; + } + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureBroadbandConnectionsDTO.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureBroadbandConnectionsDTO.java new file mode 100644 index 0000000000000..1c118af6d7f08 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureBroadbandConnectionsDTO.java @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.dto; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.THING_TYPE_BROADBAND_CONNECTION; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +import com.google.gson.annotations.SerializedName; + +/** + * The broadband connections of the Verisure System. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureBroadbandConnectionsDTO extends VerisureBaseThingDTO { + + @Override + public ThingTypeUID getThingTypeUID() { + return THING_TYPE_BROADBAND_CONNECTION; + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + return true; + } + + public static class Broadband { + + private @Nullable String testDate; + private boolean isBroadbandConnected; + @SerializedName("__typename") + private @Nullable String typename; + + public @Nullable String getTestDate() { + return testDate; + } + + public boolean isBroadbandConnected() { + return isBroadbandConnected; + } + + public @Nullable String getTypename() { + return typename; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (isBroadbandConnected ? 1231 : 1237); + String localTestDate = testDate; + result = prime * result + ((localTestDate == null) ? 0 : localTestDate.hashCode()); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Broadband other = (Broadband) obj; + if (isBroadbandConnected != other.isBroadbandConnected) { + return false; + } + String localTestDate = testDate; + if (localTestDate == null) { + if (other.testDate != null) { + return false; + } + } else if (!localTestDate.equals(other.testDate)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Broadband [testDate=" + testDate + ", isBroadbandConnected=" + isBroadbandConnected + ", typename=" + + typename + "]"; + } + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureClimatesDTO.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureClimatesDTO.java new file mode 100644 index 0000000000000..7cf7c7111f2ab --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureClimatesDTO.java @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.dto; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +import com.google.gson.annotations.SerializedName; + +/** + * The climate devices of the Verisure System. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureClimatesDTO extends VerisureBaseThingDTO { + + @Override + public ThingTypeUID getThingTypeUID() { + String type = getData().getInstallation().getClimates().get(0).getDevice().getGui().getLabel(); + if ("SMOKE".equals(type)) { + return THING_TYPE_SMOKEDETECTOR; + } else if ("WATER".equals(type)) { + return THING_TYPE_WATERDETECTOR; + } else if ("HOMEPAD".equals(type)) { + return THING_TYPE_NIGHT_CONTROL; + } else if ("SIREN".equals(type)) { + return THING_TYPE_SIREN; + } else { + return THING_TYPE_SMOKEDETECTOR; + } + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + return true; + } + + public static class Climate { + + private Device device = new Device(); + private boolean humidityEnabled; + private @Nullable String humidityTimestamp; + private double humidityValue; + private @Nullable String temperatureTimestamp; + private double temperatureValue; + @SerializedName("__typename") + private @Nullable String typename; + + public Device getDevice() { + return device; + } + + public boolean isHumidityEnabled() { + return humidityEnabled; + } + + public @Nullable String getHumidityTimestamp() { + return humidityTimestamp; + } + + public double getHumidityValue() { + return humidityValue; + } + + public @Nullable String getTemperatureTimestamp() { + return temperatureTimestamp; + } + + public double getTemperatureValue() { + return temperatureValue; + } + + public @Nullable String getTypename() { + return typename; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + device.hashCode(); + result = prime * result + (humidityEnabled ? 1231 : 1237); + String localHumidityTimestamp = humidityTimestamp; + result = prime * result + ((localHumidityTimestamp == null) ? 0 : localHumidityTimestamp.hashCode()); + long temp; + temp = Double.doubleToLongBits(humidityValue); + result = prime * result + (int) (temp ^ (temp >>> 32)); + String localTemperatureTimestamp = temperatureTimestamp; + result = prime * result + ((localTemperatureTimestamp == null) ? 0 : localTemperatureTimestamp.hashCode()); + temp = Double.doubleToLongBits(temperatureValue); + result = prime * result + (int) (temp ^ (temp >>> 32)); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Climate other = (Climate) obj; + if (!device.equals(other.device)) { + return false; + } + if (humidityEnabled != other.humidityEnabled) { + return false; + } + String localHumidityTimestamp = humidityTimestamp; + if (localHumidityTimestamp == null) { + if (other.humidityTimestamp != null) { + return false; + } + } else if (!localHumidityTimestamp.equals(other.humidityTimestamp)) { + return false; + } + if (Double.doubleToLongBits(humidityValue) != Double.doubleToLongBits(other.humidityValue)) { + return false; + } + String localTemperatureTimestamp = temperatureTimestamp; + if (localTemperatureTimestamp == null) { + if (other.temperatureTimestamp != null) { + return false; + } + } else if (!localTemperatureTimestamp.equals(other.temperatureTimestamp)) { + return false; + } + if (Double.doubleToLongBits(temperatureValue) != Double.doubleToLongBits(other.temperatureValue)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Climate [device=" + device + ", humidityEnabled=" + humidityEnabled + ", humidityTimestamp=" + + humidityTimestamp + ", humidityValue=" + humidityValue + ", temperatureTimestamp=" + + temperatureTimestamp + ", temperatureValue=" + temperatureValue + ", typename=" + typename + "]"; + } + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureDoorWindowsDTO.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureDoorWindowsDTO.java new file mode 100644 index 0000000000000..acfdf7d4b12bd --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureDoorWindowsDTO.java @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.dto; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.THING_TYPE_DOORWINDOW; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +import com.google.gson.annotations.SerializedName; + +/** + * The door and window devices of the Verisure System. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureDoorWindowsDTO extends VerisureBaseThingDTO { + + @Override + public ThingTypeUID getThingTypeUID() { + return THING_TYPE_DOORWINDOW; + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + return true; + } + + public static class DoorWindow { + + private Device device = new Device(); + private @Nullable String type; + private @Nullable String state; + private boolean wired; + private @Nullable String reportTime; + @SerializedName("__typename") + private @Nullable String typename; + + public Device getDevice() { + return device; + } + + public @Nullable String getType() { + return type; + } + + public @Nullable String getState() { + return state; + } + + public boolean getWired() { + return wired; + } + + public @Nullable String getReportTime() { + return reportTime; + } + + public @Nullable String getTypename() { + return typename; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + device.hashCode(); + String localReportTime = reportTime; + result = prime * result + ((localReportTime == null) ? 0 : localReportTime.hashCode()); + String localState = state; + result = prime * result + ((localState == null) ? 0 : localState.hashCode()); + String localType = type; + result = prime * result + ((localType == null) ? 0 : localType.hashCode()); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + result = prime * result + (wired ? 1231 : 1237); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + DoorWindow other = (DoorWindow) obj; + if (!device.equals(other.device)) { + return false; + } + String localReportTime = reportTime; + if (localReportTime == null) { + if (other.reportTime != null) { + return false; + } + } else if (!localReportTime.equals(other.reportTime)) { + return false; + } + String localState = state; + if (localState == null) { + if (other.state != null) { + return false; + } + } else if (!localState.equals(other.state)) { + return false; + } + String localType = type; + if (localType == null) { + if (other.type != null) { + return false; + } + } else if (!localType.equals(other.type)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + if (wired != other.wired) { + return false; + } + return true; + } + + @Override + public String toString() { + return "DoorWindow [device=" + device + ", type=" + type + ", state=" + state + ", wired=" + wired + + ", reportTime=" + reportTime + ", typename=" + typename + "]"; + } + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureEventLogDTO.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureEventLogDTO.java new file mode 100644 index 0000000000000..59cdee26ba56a --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureEventLogDTO.java @@ -0,0 +1,347 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.dto; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.THING_TYPE_EVENT_LOG; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The event log of the Verisure System. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureEventLogDTO extends VerisureBaseThingDTO { + + @Override + public ThingTypeUID getThingTypeUID() { + return THING_TYPE_EVENT_LOG; + } + + public static class EventLog { + + private boolean moreDataAvailable; + private List pagedList = new ArrayList<>(); + private @Nullable String typename; + + public boolean isMoreDataAvailable() { + return moreDataAvailable; + } + + public List getPagedList() { + return pagedList; + } + + public @Nullable String getTypename() { + return typename; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (moreDataAvailable ? 1231 : 1237); + result = prime * result + pagedList.hashCode(); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + EventLog other = (EventLog) obj; + if (moreDataAvailable != other.moreDataAvailable) { + return false; + } + if (!pagedList.equals(other.pagedList)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "EventLog [moreDataAvailable=" + moreDataAvailable + ", pagedList=" + pagedList + ", typename=" + + typename + "]"; + } + } + + public static class PagedList { + + private @Nullable Device device = new Device(); + private @Nullable String gatewayArea; + private @Nullable String eventType; + private @Nullable String eventCategory; + private @Nullable String eventSource; + private @Nullable String eventId; + private @Nullable String eventTime; + private @Nullable String userName; + private @Nullable String armState; + private @Nullable String userType; + private @Nullable String climateValue; + private @Nullable String sensorType; + private @Nullable String eventCount; + private @Nullable String typename; + + public @Nullable Device getDevice() { + return device; + } + + public @Nullable String getGatewayArea() { + return gatewayArea; + } + + public @Nullable String getEventType() { + return eventType; + } + + public @Nullable String getEventCategory() { + return eventCategory; + } + + public @Nullable String getEventSource() { + return eventSource; + } + + public @Nullable String getEventId() { + return eventId; + } + + public @Nullable String getEventTime() { + return eventTime; + } + + public @Nullable String getUserName() { + return userName; + } + + public @Nullable String getArmState() { + return armState; + } + + public @Nullable String getUserType() { + return userType; + } + + public @Nullable String getClimateValue() { + return climateValue; + } + + public @Nullable String getSensorType() { + return sensorType; + } + + public @Nullable String getEventCount() { + return eventCount; + } + + public @Nullable String getTypename() { + return typename; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + String localArmState = armState; + result = prime * result + ((localArmState == null) ? 0 : localArmState.hashCode()); + String localClimateValue = climateValue; + result = prime * result + ((localClimateValue == null) ? 0 : localClimateValue.hashCode()); + Device localDevice = device; + result = prime * result + ((localDevice == null) ? 0 : localDevice.hashCode()); + String localEventCategory = eventCategory; + result = prime * result + ((localEventCategory == null) ? 0 : localEventCategory.hashCode()); + String localEventCount = eventCount; + result = prime * result + ((localEventCount == null) ? 0 : localEventCount.hashCode()); + String localEventId = eventId; + result = prime * result + ((localEventId == null) ? 0 : localEventId.hashCode()); + String localEventSource = eventSource; + result = prime * result + ((localEventSource == null) ? 0 : localEventSource.hashCode()); + String localEventTime = eventTime; + result = prime * result + ((localEventTime == null) ? 0 : localEventTime.hashCode()); + String localEventType = eventType; + result = prime * result + ((localEventType == null) ? 0 : localEventType.hashCode()); + String localGatewayArea = gatewayArea; + result = prime * result + ((localGatewayArea == null) ? 0 : localGatewayArea.hashCode()); + String localSensorType = sensorType; + result = prime * result + ((localSensorType == null) ? 0 : localSensorType.hashCode()); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + String localUserName = userName; + result = prime * result + ((localUserName == null) ? 0 : localUserName.hashCode()); + String localUserType = userType; + result = prime * result + ((localUserType == null) ? 0 : localUserType.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + PagedList other = (PagedList) obj; + String localArmState = armState; + if (localArmState == null) { + if (other.armState != null) { + return false; + } + } else if (!localArmState.equals(other.armState)) { + return false; + } + String localClimateValue = climateValue; + if (localClimateValue == null) { + if (other.climateValue != null) { + return false; + } + } else if (!localClimateValue.equals(other.climateValue)) { + return false; + } + Device localDevice = device; + if (localDevice == null) { + if (other.device != null) { + return false; + } + } else if (!localDevice.equals(other.device)) { + return false; + } + String localEventCategory = eventCategory; + if (localEventCategory == null) { + if (other.eventCategory != null) { + return false; + } + } else if (!localEventCategory.equals(other.eventCategory)) { + return false; + } + String localEventCount = eventCount; + if (localEventCount == null) { + if (other.eventCount != null) { + return false; + } + } else if (!localEventCount.equals(other.eventCount)) { + return false; + } + String localEventId = eventId; + if (localEventId == null) { + if (other.eventId != null) { + return false; + } + } else if (!localEventId.equals(other.eventId)) { + return false; + } + String localEventSource = eventSource; + if (localEventSource == null) { + if (other.eventSource != null) { + return false; + } + } else if (!localEventSource.equals(other.eventSource)) { + return false; + } + String localEventTime = eventTime; + if (localEventTime == null) { + if (other.eventTime != null) { + return false; + } + } else if (!localEventTime.equals(other.eventTime)) { + return false; + } + String localEventType = eventType; + if (localEventType == null) { + if (other.eventType != null) { + return false; + } + } else if (!localEventType.equals(other.eventType)) { + return false; + } + String localGatewayArea = gatewayArea; + if (localGatewayArea == null) { + if (other.gatewayArea != null) { + return false; + } + } else if (!localGatewayArea.equals(other.gatewayArea)) { + return false; + } + String localSensorType = sensorType; + if (localSensorType == null) { + if (other.sensorType != null) { + return false; + } + } else if (!localSensorType.equals(other.sensorType)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + String localUserName = userName; + if (localUserName == null) { + if (other.userName != null) { + return false; + } + } else if (!localUserName.equals(other.userName)) { + return false; + } + String localUserType = userType; + if (localUserType == null) { + if (other.userType != null) { + return false; + } + } else if (!localUserType.equals(other.userType)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "PagedList [device=" + device + ", gatewayArea=" + gatewayArea + ", eventType=" + eventType + + ", eventCategory=" + eventCategory + ", eventSource=" + eventSource + ", eventId=" + eventId + + ", eventTime=" + eventTime + ", userName=" + userName + ", armState=" + armState + ", userType=" + + userType + ", climateValue=" + climateValue + ", sensorType=" + sensorType + ", eventCount=" + + eventCount + ", typename=" + typename + "]"; + } + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureGatewayDTO.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureGatewayDTO.java new file mode 100644 index 0000000000000..9f92fd1549d37 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureGatewayDTO.java @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.dto; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.THING_TYPE_GATEWAY; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The gateway in the Verisure System. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureGatewayDTO extends VerisureBaseThingDTO { + + @Override + public ThingTypeUID getThingTypeUID() { + return THING_TYPE_GATEWAY; + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + return true; + } + + public static class CommunicationState { + + private @Nullable String hardwareCarrierType; + private @Nullable String result; + private @Nullable String mediaType; + private Device device = new Device(); + private @Nullable String testDate; + private @Nullable String typename; + + public @Nullable String getHardwareCarrierType() { + return hardwareCarrierType; + } + + public @Nullable String getResult() { + return result; + } + + public @Nullable String getMediaType() { + return mediaType; + } + + public Device getDevice() { + return device; + } + + public @Nullable String getTestDate() { + return testDate; + } + + public @Nullable String getTypename() { + return typename; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + device.hashCode(); + String locaHardwareCarrierType = hardwareCarrierType; + result = prime * result + ((locaHardwareCarrierType == null) ? 0 : locaHardwareCarrierType.hashCode()); + String localMediaType = mediaType; + result = prime * result + ((localMediaType == null) ? 0 : localMediaType.hashCode()); + String localResult = this.result; + result = prime * result + ((localResult == null) ? 0 : localResult.hashCode()); + String localTestDate = testDate; + result = prime * result + ((localTestDate == null) ? 0 : localTestDate.hashCode()); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CommunicationState other = (CommunicationState) obj; + if (!device.equals(other.device)) { + return false; + } + String locaHardwareCarrierType = hardwareCarrierType; + if (locaHardwareCarrierType == null) { + if (other.hardwareCarrierType != null) { + return false; + } + } else if (!locaHardwareCarrierType.equals(other.hardwareCarrierType)) { + return false; + } + String localMediaType = mediaType; + if (localMediaType == null) { + if (other.mediaType != null) { + return false; + } + } else if (!localMediaType.equals(other.mediaType)) { + return false; + } + String localResult = result; + if (localResult == null) { + if (other.result != null) { + return false; + } + } else if (!localResult.equals(other.result)) { + return false; + } + String localTestDate = testDate; + if (localTestDate == null) { + if (other.testDate != null) { + return false; + } + } else if (!localTestDate.equals(other.testDate)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "CommunicationState [hardwareCarrierType=" + hardwareCarrierType + ", result=" + result + + ", mediaType=" + mediaType + ", device=" + device + ", testDate=" + testDate + ", typename=" + + typename + "]"; + } + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureInstallationsDTO.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureInstallationsDTO.java new file mode 100644 index 0000000000000..afd55f563bdb4 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureInstallationsDTO.java @@ -0,0 +1,300 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.dto; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.verisure.internal.dto.VerisureBaseThingDTO.Installation; + +import com.google.gson.annotations.SerializedName; + +/** + * The installation(s) of the Verisure System. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureInstallationsDTO { + + private Data data = new Data(); + + public Data getData() { + return data; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + data.hashCode(); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + VerisureInstallationsDTO other = (VerisureInstallationsDTO) obj; + if (!data.equals(other.data)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "VerisureInstallationsDTO [data=" + data + "]"; + } + + public static class Data { + private Installation installation = new Installation(); + private Account account = new Account(); + + public Account getAccount() { + return account; + } + + public Installation getInstallation() { + return installation; + } + + public void setInstallation(Installation installation) { + this.installation = installation; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + account.hashCode(); + result = prime * result + installation.hashCode(); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Data other = (Data) obj; + if (!account.equals(other.account)) { + return false; + } + if (!installation.equals(other.installation)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Data [installation=" + installation + ", account=" + account + "]"; + } + } + + public static class Account { + + @SerializedName("__typename") + private @Nullable String typename; + private List owainstallations = new ArrayList<>(); + + public @Nullable String getTypename() { + return typename; + } + + public List getOwainstallations() { + return owainstallations; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + owainstallations.hashCode(); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Account other = (Account) obj; + if (!owainstallations.equals(other.owainstallations)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Account [typename=" + typename + ", owainstallations=" + owainstallations + "]"; + } + } + + public static class Owainstallation { + + @SerializedName("__typename") + private @Nullable String typename; + private @Nullable String alias; + private @Nullable String dealerId; + private @Nullable String giid; + private @Nullable String subsidiary; + private @Nullable String type; + + public @Nullable String getTypename() { + return typename; + } + + public @Nullable String getAlias() { + return alias; + } + + public @Nullable String getDealerId() { + return dealerId; + } + + public @Nullable String getGiid() { + return giid; + } + + public @Nullable String getSubsidiary() { + return subsidiary; + } + + public @Nullable String getType() { + return type; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + String localAlias = alias; + result = prime * result + ((localAlias == null) ? 0 : localAlias.hashCode()); + String localDealerId = dealerId; + result = prime * result + ((localDealerId == null) ? 0 : localDealerId.hashCode()); + String localGiid = giid; + result = prime * result + ((localGiid == null) ? 0 : localGiid.hashCode()); + String localSubsidiary = subsidiary; + result = prime * result + ((localSubsidiary == null) ? 0 : localSubsidiary.hashCode()); + String localType = type; + result = prime * result + ((localType == null) ? 0 : localType.hashCode()); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Owainstallation other = (Owainstallation) obj; + String localAlias = alias; + if (localAlias == null) { + if (other.alias != null) { + return false; + } + } else if (!localAlias.equals(other.alias)) { + return false; + } + String localDealerId = dealerId; + if (localDealerId == null) { + if (other.dealerId != null) { + return false; + } + } else if (!localDealerId.equals(other.dealerId)) { + return false; + } + String localGiid = giid; + if (localGiid == null) { + if (other.giid != null) { + return false; + } + } else if (!localGiid.equals(other.giid)) { + return false; + } + String localSubsidiary = subsidiary; + if (localSubsidiary == null) { + if (other.subsidiary != null) { + return false; + } + } else if (!localSubsidiary.equals(other.subsidiary)) { + return false; + } + String localType = type; + if (localType == null) { + if (other.type != null) { + return false; + } + } else if (!localType.equals(other.type)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Owainstallation [typename=" + typename + ", alias=" + alias + ", dealerId=" + dealerId + ", giid=" + + giid + ", subsidiary=" + subsidiary + ", type=" + type + "]"; + } + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureMiceDetectionDTO.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureMiceDetectionDTO.java new file mode 100644 index 0000000000000..68b7837e7fedd --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureMiceDetectionDTO.java @@ -0,0 +1,279 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.dto; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.THING_TYPE_MICE_DETECTION; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The Mice detection status of the Verisure System. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureMiceDetectionDTO extends VerisureBaseThingDTO { + + public static final int UNDEFINED = -1; + private double temperatureValue = UNDEFINED; + private @Nullable String temperatureTimestamp; + + public double getTemperatureValue() { + return temperatureValue; + } + + public void setTemperatureValue(double temperatureValue) { + this.temperatureValue = temperatureValue; + } + + public @Nullable String getTemperatureTime() { + return temperatureTimestamp; + } + + public void setTemperatureTime(@Nullable String temperatureTimestamp) { + this.temperatureTimestamp = temperatureTimestamp; + } + + @Override + public ThingTypeUID getThingTypeUID() { + return THING_TYPE_MICE_DETECTION; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + String localTemperatureTimestamp = temperatureTimestamp; + result = prime * result + ((localTemperatureTimestamp == null) ? 0 : localTemperatureTimestamp.hashCode()); + long temp; + temp = Double.doubleToLongBits(temperatureValue); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + VerisureMiceDetectionDTO other = (VerisureMiceDetectionDTO) obj; + String localTemperatureTimestamp = temperatureTimestamp; + if (localTemperatureTimestamp == null) { + if (other.temperatureTimestamp != null) { + return false; + } + } else if (!localTemperatureTimestamp.equals(other.temperatureTimestamp)) { + return false; + } + if (Double.doubleToLongBits(temperatureValue) != Double.doubleToLongBits(other.temperatureValue)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "VerisureMiceDetectionDTO [temperatureValue=" + temperatureValue + ", temperatureTimestamp=" + + temperatureTimestamp + "]"; + } + + public static class Mouse { + + private Device device = new Device(); + private @Nullable Object type; + private List detections = new ArrayList<>(); + private @Nullable String typename; + + public Device getDevice() { + return device; + } + + public @Nullable Object getType() { + return type; + } + + public List getDetections() { + return detections; + } + + public @Nullable String getTypename() { + return typename; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + detections.hashCode(); + result = prime * result + device.hashCode(); + Object localType = type; + result = prime * result + ((localType == null) ? 0 : localType.hashCode()); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Mouse other = (Mouse) obj; + if (!detections.equals(other.detections)) { + return false; + } + if (!device.equals(other.device)) { + return false; + } + Object localType = type; + if (localType == null) { + if (other.type != null) { + return false; + } + } else if (!localType.equals(other.type)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Mouse [device=" + device + ", type=" + type + ", detections=" + detections + ", typename=" + + typename + "]"; + } + } + + public static class Detection { + + private int count; + private @Nullable String gatewayTime; + private @Nullable String nodeTime; + private int duration; + private @Nullable String typename; + + public int getCount() { + return count; + } + + public @Nullable String getGatewayTime() { + return gatewayTime; + } + + public @Nullable String getNodeTime() { + return nodeTime; + } + + public int getDuration() { + return duration; + } + + public @Nullable String getTypename() { + return typename; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + count; + result = prime * result + duration; + String localGatewayTime = gatewayTime; + result = prime * result + ((localGatewayTime == null) ? 0 : localGatewayTime.hashCode()); + String localNodeTime = nodeTime; + result = prime * result + ((localNodeTime == null) ? 0 : localNodeTime.hashCode()); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Detection other = (Detection) obj; + if (count != other.count) { + return false; + } + if (duration != other.duration) { + return false; + } + String localGatewayTime = gatewayTime; + if (localGatewayTime == null) { + if (other.gatewayTime != null) { + return false; + } + } else if (!localGatewayTime.equals(other.gatewayTime)) { + return false; + } + String localNodeTime = nodeTime; + if (localNodeTime == null) { + if (other.nodeTime != null) { + return false; + } + } else if (!localNodeTime.equals(other.nodeTime)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Detection [count=" + count + ", gatewayTime=" + gatewayTime + ", nodeTime=" + nodeTime + + ", duration=" + duration + ", typename=" + typename + "]"; + } + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureSmartLockDTO.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureSmartLockDTO.java new file mode 100644 index 0000000000000..5f6ad3a73f991 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureSmartLockDTO.java @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.dto; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The SmartLock state of the Verisure System. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureSmartLockDTO { + + private boolean autoRelockEnabled; + private @Nullable String deviceLabel; + private DoorLockVolumeSettings doorLockVolumeSettings = new DoorLockVolumeSettings(); + + public boolean getAutoRelockEnabled() { + return autoRelockEnabled; + } + + public @Nullable String getDeviceLabel() { + return deviceLabel; + } + + public DoorLockVolumeSettings getDoorLockVolumeSettings() { + return doorLockVolumeSettings; + } + + public static class DoorLockVolumeSettings { + private @Nullable String volume; + private @Nullable String voiceLevel; + private @Nullable String active; + private List availableVolumes = new ArrayList<>(); + private List availableVoiceLevels = new ArrayList<>(); + + public @Nullable String getVolume() { + return volume; + } + + public @Nullable String getVoiceLevel() { + return voiceLevel; + } + + public @Nullable String getActive() { + return active; + } + + public List getAvailableVolumes() { + return availableVolumes; + } + + public List getAvailableVoiceLevels() { + return availableVoiceLevels; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + String localActive = active; + result = prime * result + ((localActive == null) ? 0 : localActive.hashCode()); + result = prime * result + availableVoiceLevels.hashCode(); + result = prime * result + availableVolumes.hashCode(); + String localVoiceLevel = voiceLevel; + result = prime * result + ((localVoiceLevel == null) ? 0 : localVoiceLevel.hashCode()); + String localVolume = volume; + result = prime * result + ((localVolume == null) ? 0 : localVolume.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + DoorLockVolumeSettings other = (DoorLockVolumeSettings) obj; + String localActive = active; + if (localActive == null) { + if (other.active != null) { + return false; + } + } else if (!localActive.equals(other.active)) { + return false; + } + if (!availableVoiceLevels.equals(other.availableVoiceLevels)) { + return false; + } + if (!availableVolumes.equals(other.availableVolumes)) { + return false; + } + String localVoiceLevel = voiceLevel; + if (localVoiceLevel == null) { + if (other.voiceLevel != null) { + return false; + } + } else if (!localVoiceLevel.equals(other.voiceLevel)) { + return false; + } + String localVolume = volume; + if (localVolume == null) { + if (other.volume != null) { + return false; + } + } else if (!localVolume.equals(other.volume)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "DoorLockVolumeSettings [volume=" + volume + ", voiceLevel=" + voiceLevel + ", active=" + active + + ", availableVolumes=" + availableVolumes + ", availableVoiceLevels=" + availableVoiceLevels + "]"; + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (autoRelockEnabled ? 1231 : 1237); + String localDeviceLabel = deviceLabel; + result = prime * result + ((localDeviceLabel == null) ? 0 : localDeviceLabel.hashCode()); + result = prime * result + doorLockVolumeSettings.hashCode(); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + VerisureSmartLockDTO other = (VerisureSmartLockDTO) obj; + if (autoRelockEnabled != other.autoRelockEnabled) { + return false; + } + String localDeviceLabel = deviceLabel; + if (localDeviceLabel == null) { + if (other.deviceLabel != null) { + return false; + } + } else if (!localDeviceLabel.equals(other.deviceLabel)) { + return false; + } + if (!doorLockVolumeSettings.equals(other.doorLockVolumeSettings)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "VerisureSmartLockDTO [autoRelockEnabled=" + autoRelockEnabled + ", deviceLabel=" + deviceLabel + + ", doorLockVolumeSettings=" + doorLockVolumeSettings + "]"; + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureSmartLocksDTO.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureSmartLocksDTO.java new file mode 100644 index 0000000000000..2613496698cf1 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureSmartLocksDTO.java @@ -0,0 +1,222 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.dto; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.THING_TYPE_SMARTLOCK; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +import com.google.gson.annotations.SerializedName; + +/** + * The smart locks of the Verisure System. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureSmartLocksDTO extends VerisureBaseThingDTO { + + private @Nullable VerisureSmartLockDTO smartLockJSON; + + public @Nullable VerisureSmartLockDTO getSmartLockJSON() { + return smartLockJSON; + } + + public void setSmartLockJSON(@Nullable VerisureSmartLockDTO smartLockJSON) { + this.smartLockJSON = smartLockJSON; + } + + @Override + public ThingTypeUID getThingTypeUID() { + return THING_TYPE_SMARTLOCK; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + VerisureSmartLockDTO localSmartLockJSON = smartLockJSON; + result = prime * result + ((localSmartLockJSON == null) ? 0 : localSmartLockJSON.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + VerisureSmartLocksDTO other = (VerisureSmartLocksDTO) obj; + VerisureSmartLockDTO localSmartLockJSON = smartLockJSON; + if (localSmartLockJSON == null) { + if (other.smartLockJSON != null) { + return false; + } + } else if (!localSmartLockJSON.equals(other.smartLockJSON)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "VerisureSmartLocksDTO [smartLockJSON=" + smartLockJSON + "]"; + } + + public static class Doorlock { + + @SerializedName("__typename") + private @Nullable String typename; + private @Nullable String currentLockState; + private @Nullable String eventTime; + private @Nullable String method; + private @Nullable String userString; + private Device device = new Device(); + private boolean motorJam; + private boolean secureModeActive; + + public @Nullable String getTypename() { + return typename; + } + + public @Nullable String getCurrentLockState() { + return currentLockState; + } + + public Device getDevice() { + return device; + } + + public @Nullable String getEventTime() { + return eventTime; + } + + public @Nullable String getMethod() { + return method; + } + + public boolean isMotorJam() { + return motorJam; + } + + public boolean getSecureModeActive() { + return secureModeActive; + } + + public @Nullable String getUserString() { + return userString; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + String localCurrentLockState = currentLockState; + result = prime * result + ((localCurrentLockState == null) ? 0 : localCurrentLockState.hashCode()); + result = prime * result + device.hashCode(); + String localEventTime = eventTime; + result = prime * result + ((localEventTime == null) ? 0 : localEventTime.hashCode()); + String localMethod = method; + result = prime * result + ((localMethod == null) ? 0 : localMethod.hashCode()); + result = prime * result + (motorJam ? 1231 : 1237); + result = prime * result + (secureModeActive ? 1231 : 1237); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + String localUserString = userString; + result = prime * result + ((localUserString == null) ? 0 : localUserString.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Doorlock other = (Doorlock) obj; + String localCurrentLockState = currentLockState; + if (localCurrentLockState == null) { + if (other.currentLockState != null) { + return false; + } + } else if (!localCurrentLockState.equals(other.currentLockState)) { + return false; + } + if (!device.equals(other.device)) { + return false; + } + String localEventTime = eventTime; + if (localEventTime == null) { + if (other.eventTime != null) { + return false; + } + } else if (!localEventTime.equals(other.eventTime)) { + return false; + } + String localMethod = method; + if (localMethod == null) { + if (other.method != null) { + return false; + } + } else if (!localMethod.equals(other.method)) { + return false; + } + if (motorJam != other.motorJam) { + return false; + } + if (secureModeActive != other.secureModeActive) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + String localUserString = userString; + if (localUserString == null) { + if (other.userString != null) { + return false; + } + } else if (!localUserString.equals(other.userString)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Doorlock [typename=" + typename + ", currentLockState=" + currentLockState + ", eventTime=" + + eventTime + ", method=" + method + ", userString=" + userString + ", device=" + device + + ", motorJam=" + motorJam + ", secureModeActive=" + secureModeActive + "]"; + } + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureSmartPlugsDTO.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureSmartPlugsDTO.java new file mode 100644 index 0000000000000..3f1b788ac1203 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureSmartPlugsDTO.java @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.dto; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.THING_TYPE_SMARTPLUG; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +import com.google.gson.annotations.SerializedName; + +/** + * The smart plugs of the Verisure System. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureSmartPlugsDTO extends VerisureBaseThingDTO { + + @Override + public ThingTypeUID getThingTypeUID() { + return THING_TYPE_SMARTPLUG; + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + return true; + } + + public static class Smartplug { + + private Device device = new Device(); + private @Nullable String currentState; + private @Nullable String icon; + private boolean isHazardous; + @SerializedName("__typename") + private @Nullable String typename; + + public Device getDevice() { + return device; + } + + public @Nullable String getCurrentState() { + return currentState; + } + + public @Nullable String getIcon() { + return icon; + } + + public boolean isHazardous() { + return isHazardous; + } + + public @Nullable String getTypename() { + return typename; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + String localCurrentState = currentState; + result = prime * result + ((localCurrentState == null) ? 0 : localCurrentState.hashCode()); + result = prime * result + device.hashCode(); + String localIcon = icon; + result = prime * result + ((localIcon == null) ? 0 : localIcon.hashCode()); + result = prime * result + (isHazardous ? 1231 : 1237); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Smartplug other = (Smartplug) obj; + String localCurrentState = currentState; + if (localCurrentState == null) { + if (other.currentState != null) { + return false; + } + } else if (!localCurrentState.equals(other.currentState)) { + return false; + } + if (!device.equals(other.device)) { + return false; + } + String localIcon = icon; + if (localIcon == null) { + if (other.icon != null) { + return false; + } + } else if (!localIcon.equals(other.icon)) { + return false; + } + if (isHazardous != other.isHazardous) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Smartplug [device=" + device + ", currentState=" + currentState + ", icon=" + icon + + ", isHazardous=" + isHazardous + ", typename=" + typename + "]"; + } + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureThingDTO.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureThingDTO.java new file mode 100644 index 0000000000000..996611d3bc128 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureThingDTO.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.dto; + +import java.math.BigDecimal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The base identifer of all Verisure response objects. + * + * @author Jarle Hjortland - Initial contribution + * @author Jan Gustafsson - Further development + * + */ +@NonNullByDefault +public interface VerisureThingDTO { + + String getDeviceId(); + + void setDeviceId(String deviceId); + + @Nullable + String getLocation(); + + void setSiteName(@Nullable String siteName); + + @Nullable + String getSiteName(); + + void setSiteId(BigDecimal siteId); + + BigDecimal getSiteId(); + + ThingTypeUID getThingTypeUID(); +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureUserPresencesDTO.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureUserPresencesDTO.java new file mode 100644 index 0000000000000..dba37994f99eb --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/dto/VerisureUserPresencesDTO.java @@ -0,0 +1,247 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.dto; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.THING_TYPE_USERPRESENCE; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +import com.google.gson.annotations.SerializedName; + +/** + * The user presences of the Verisure System. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureUserPresencesDTO extends VerisureBaseThingDTO { + + @Override + public ThingTypeUID getThingTypeUID() { + return THING_TYPE_USERPRESENCE; + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + return true; + } + + public static class UserTracking { + private boolean isCallingUser; + private @Nullable String webAccount; + private @Nullable String status; + private @Nullable String xbnContactId; + private @Nullable String currentLocationName; + private String deviceId = ""; + private @Nullable String name; + private @Nullable String currentLocationTimestamp; + private @Nullable String deviceName; + private @Nullable String currentLocationId; + @SerializedName("__typename") + private @Nullable String typename; + + public boolean getIsCallingUser() { + return isCallingUser; + } + + public @Nullable String getWebAccount() { + return webAccount; + } + + public @Nullable String getStatus() { + return status; + } + + public @Nullable String getXbnContactId() { + return xbnContactId; + } + + public @Nullable String getCurrentLocationName() { + return currentLocationName; + } + + public @Nullable String getDeviceId() { + return deviceId; + } + + public @Nullable String getName() { + return name; + } + + public @Nullable String getCurrentLocationTimestamp() { + return currentLocationTimestamp; + } + + public @Nullable String getDeviceName() { + return deviceName; + } + + public @Nullable String getCurrentLocationId() { + return currentLocationId; + } + + public @Nullable String getTypename() { + return typename; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + String localCurrentLocationId = currentLocationId; + result = prime * result + ((localCurrentLocationId == null) ? 0 : localCurrentLocationId.hashCode()); + String localCurrentLocationName = currentLocationName; + result = prime * result + ((localCurrentLocationName == null) ? 0 : localCurrentLocationName.hashCode()); + String localCurrentLocationTimestamp = currentLocationTimestamp; + result = prime * result + + ((localCurrentLocationTimestamp == null) ? 0 : localCurrentLocationTimestamp.hashCode()); + result = prime * result + deviceId.hashCode(); + String localDeviceName = deviceName; + result = prime * result + ((localDeviceName == null) ? 0 : localDeviceName.hashCode()); + result = prime * result + (isCallingUser ? 1231 : 1237); + String localName = name; + result = prime * result + ((localName == null) ? 0 : localName.hashCode()); + String localStatus = status; + result = prime * result + ((localStatus == null) ? 0 : localStatus.hashCode()); + String localTypeName = typename; + result = prime * result + ((localTypeName == null) ? 0 : localTypeName.hashCode()); + String localWebAccount = webAccount; + result = prime * result + ((localWebAccount == null) ? 0 : localWebAccount.hashCode()); + String localXbnContactId = xbnContactId; + result = prime * result + ((localXbnContactId == null) ? 0 : localXbnContactId.hashCode()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + UserTracking other = (UserTracking) obj; + String localCurrentLocationId = currentLocationId; + if (localCurrentLocationId == null) { + if (other.currentLocationId != null) { + return false; + } + } else if (!localCurrentLocationId.equals(other.currentLocationId)) { + return false; + } + String localCurrentLocationName = currentLocationName; + if (localCurrentLocationName == null) { + if (other.currentLocationName != null) { + return false; + } + } else if (!localCurrentLocationName.equals(other.currentLocationName)) { + return false; + } + String localCurrentLocationTimestamp = currentLocationTimestamp; + if (localCurrentLocationTimestamp == null) { + if (other.currentLocationTimestamp != null) { + return false; + } + } else if (!localCurrentLocationTimestamp.equals(other.currentLocationTimestamp)) { + return false; + } + if (!deviceId.equals(other.deviceId)) { + return false; + } + String localDeviceName = deviceName; + if (localDeviceName == null) { + if (other.deviceName != null) { + return false; + } + } else if (!localDeviceName.equals(other.deviceName)) { + return false; + } + if (isCallingUser != other.isCallingUser) { + return false; + } + String localName = name; + if (localName == null) { + if (other.name != null) { + return false; + } + } else if (!localName.equals(other.name)) { + return false; + } + String localStatus = status; + if (localStatus == null) { + if (other.status != null) { + return false; + } + } else if (!localStatus.equals(other.status)) { + return false; + } + String localTypeName = typename; + if (localTypeName == null) { + if (other.typename != null) { + return false; + } + } else if (!localTypeName.equals(other.typename)) { + return false; + } + String localWebAccount = webAccount; + if (localWebAccount == null) { + if (other.webAccount != null) { + return false; + } + } else if (!localWebAccount.equals(other.webAccount)) { + return false; + } + String localXbnContactId = xbnContactId; + if (localXbnContactId == null) { + if (other.xbnContactId != null) { + return false; + } + } else if (!localXbnContactId.equals(other.xbnContactId)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "UserTracking [isCallingUser=" + isCallingUser + ", webAccount=" + webAccount + ", status=" + status + + ", xbnContactId=" + xbnContactId + ", currentLocationName=" + currentLocationName + ", deviceId=" + + deviceId + ", name=" + name + ", currentLocationTimestamp=" + currentLocationTimestamp + + ", deviceName=" + deviceName + ", currentLocationId=" + currentLocationId + ", typename=" + + typename + "]"; + } + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureAlarmThingHandler.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureAlarmThingHandler.java new file mode 100644 index 0000000000000..ca245a32672f8 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureAlarmThingHandler.java @@ -0,0 +1,213 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.handler; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.verisure.internal.VerisureSession; +import org.openhab.binding.verisure.internal.dto.VerisureAlarmsDTO; +import org.openhab.binding.verisure.internal.dto.VerisureAlarmsDTO.ArmState; + +/** + * Handler for the Alarm Device thing type that Verisure provides. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureAlarmThingHandler extends VerisureThingHandler { + + public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_ALARM); + + private static final int REFRESH_DELAY_SECONDS = 10; + + public VerisureAlarmThingHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("handleCommand, channel: {}, command: {}", channelUID, command); + if (command instanceof RefreshType) { + super.handleCommand(channelUID, command); + } else if (channelUID.getId().equals(CHANNEL_ALARM_STATUS)) { + handleAlarmState(command); + scheduleImmediateRefresh(REFRESH_DELAY_SECONDS); + } else { + logger.warn("Unknown command! {}", command); + } + } + + private void handleAlarmState(Command command) { + String deviceId = config.getDeviceId(); + VerisureSession session = getSession(); + if (session != null) { + VerisureAlarmsDTO alarm = session.getVerisureThing(deviceId, getVerisureThingClass()); + if (alarm != null) { + BigDecimal installationId = alarm.getSiteId(); + String pinCode = session.getPinCode(installationId); + + if (pinCode != null) { + String url = START_GRAPHQL; + String operation, state = ""; + + switch (command.toString()) { + case "DISARMED": + operation = "disarm"; + state = "armStateDisarm"; + break; + case "ARMED_HOME": + operation = "armHome"; + state = "armStateArmHome"; + break; + case "ARMED_AWAY": + operation = "armAway"; + state = "armStateArmAway"; + break; + default: + logger.warn("Unknown alarm command: {}", command); + return; + } + + ArrayList list = new ArrayList<>(); + AlarmDTO alarmJSON = new AlarmDTO(); + VariablesDTO variables = new VariablesDTO(); + + variables.setCode(pinCode); + variables.setGiid(installationId.toString()); + alarmJSON.setVariables(variables); + alarmJSON.setOperationName(operation); + String query = "mutation " + operation + "($giid: String!, $code: String!) {\n " + state + + "(giid: $giid, code: $code)\n}\n"; + alarmJSON.setQuery(query); + list.add(alarmJSON); + + String queryQLAlarmSetState = gson.toJson(list); + logger.debug("Trying to set alarm state to {} with URL {} and data {}", operation, url, + queryQLAlarmSetState); + + int httpResultCode = session.sendCommand(url, queryQLAlarmSetState, installationId); + if (httpResultCode == HttpStatus.OK_200) { + logger.debug("Alarm status successfully changed!"); + } else { + logger.warn("Could not send command, HTTP result code: {}", httpResultCode); + } + } else { + logger.warn("PIN code is not configured! Mandatory to control Alarm!"); + } + } + } + } + + @Override + public Class getVerisureThingClass() { + return VerisureAlarmsDTO.class; + } + + @Override + public synchronized void update(VerisureAlarmsDTO thing) { + updateAlarmState(thing); + updateStatus(ThingStatus.ONLINE); + } + + private void updateAlarmState(VerisureAlarmsDTO alarmsJSON) { + ArmState armState = alarmsJSON.getData().getInstallation().getArmState(); + String alarmStatus = armState.getStatusType(); + if (alarmStatus != null) { + getThing().getChannels().stream().map(Channel::getUID) + .filter(channelUID -> isLinked(channelUID) && !channelUID.getId().equals("timestamp")) + .forEach(channelUID -> { + State state = getValue(channelUID.getId(), armState); + updateState(channelUID, state); + }); + updateTimeStamp(armState.getDate()); + updateInstallationChannels(alarmsJSON); + } else { + logger.warn("Alarm status is null!"); + } + } + + public State getValue(String channelId, ArmState armState) { + switch (channelId) { + case CHANNEL_ALARM_STATUS: + return new StringType(armState.getStatusType()); + case CHANNEL_CHANGED_BY_USER: + return new StringType(armState.getName()); + case CHANNEL_CHANGED_VIA: + return new StringType(armState.getChangedVia()); + } + return UnDefType.UNDEF; + } + + private static class AlarmDTO { + + @SuppressWarnings("unused") + private @Nullable String operationName; + @SuppressWarnings("unused") + private VariablesDTO variables = new VariablesDTO(); + @SuppressWarnings("unused") + private @Nullable String query; + + public void setOperationName(String operationName) { + this.operationName = operationName; + } + + public void setVariables(VariablesDTO variables) { + this.variables = variables; + } + + public void setQuery(String query) { + this.query = query; + } + } + + private static class VariablesDTO { + + @SuppressWarnings("unused") + private @Nullable String giid; + @SuppressWarnings("unused") + private @Nullable String code; + + public void setGiid(String giid) { + this.giid = giid; + } + + public void setCode(String code) { + this.code = code; + } + } + + @Override + public void updateTriggerChannel(String event) { + logger.debug("ThingHandler trigger event {}", event); + triggerChannel(CHANNEL_SMARTLOCK_TRIGGER_CHANNEL, event); + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureBridgeHandler.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureBridgeHandler.java new file mode 100644 index 0000000000000..cc901ae15cf2a --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureBridgeHandler.java @@ -0,0 +1,266 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.handler; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.openhab.binding.verisure.internal.DeviceStatusListener; +import org.openhab.binding.verisure.internal.VerisureBridgeConfiguration; +import org.openhab.binding.verisure.internal.VerisureSession; +import org.openhab.binding.verisure.internal.discovery.VerisureThingDiscoveryService; +import org.openhab.binding.verisure.internal.dto.VerisureThingDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link VerisureBridgeHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author l3rum - Initial contribution + * @author Jan Gustafsson - Furher development + */ +@NonNullByDefault +public class VerisureBridgeHandler extends BaseBridgeHandler { + + public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE); + + private static final int REFRESH_DELAY_SECONDS = 30; + private final Logger logger = LoggerFactory.getLogger(VerisureBridgeHandler.class); + private final ReentrantLock immediateRefreshJobLock = new ReentrantLock(); + private final HttpClient httpClient; + + private String authstring = ""; + private @Nullable String pinCode; + private static int REFRESH_SEC = 600; + private @Nullable ScheduledFuture refreshJob; + private @Nullable ScheduledFuture immediateRefreshJob; + private @Nullable VerisureSession session; + + public VerisureBridgeHandler(Bridge bridge, HttpClient httpClient) { + super(bridge); + this.httpClient = httpClient; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("VerisureBridgeHandler Handle command {} on channelUID: {}", command, channelUID); + if (command instanceof RefreshType) { + if (channelUID.getId().equals(CHANNEL_STATUS) && channelUID.getThingUID().equals(getThing().getUID())) { + logger.debug("Refresh command on status channel {} will trigger instant refresh", channelUID); + scheduleImmediateRefresh(0); + } else { + logger.debug("Refresh command on channel {} will trigger refresh in {} seconds", channelUID, + REFRESH_DELAY_SECONDS); + scheduleImmediateRefresh(REFRESH_DELAY_SECONDS); + } + } else { + logger.warn("unknown command! {}", command); + } + } + + public @Nullable VerisureSession getSession() { + return session; + } + + public @Nullable ThingUID getUID() { + return getThing().getUID(); + } + + @Override + public void initialize() { + logger.debug("Initializing Verisure Binding"); + VerisureBridgeConfiguration config = getConfigAs(VerisureBridgeConfiguration.class); + REFRESH_SEC = config.refresh; + this.pinCode = config.pin; + if (config.username == null || config.password == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Configuration of username and password is mandatory"); + } else if (REFRESH_SEC < 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Refresh time cannot negative!"); + } else { + try { + authstring = "j_username=" + config.username + "&j_password=" + + URLEncoder.encode(config.password, StandardCharsets.UTF_8.toString()) + + "&spring-security-redirect=" + START_REDIRECT; + scheduler.execute(() -> { + + if (session == null) { + logger.debug("Session is null, let's create a new one"); + session = new VerisureSession(this.httpClient); + } + VerisureSession session = this.session; + updateStatus(ThingStatus.UNKNOWN); + if (session != null) { + if (!session.initialize(authstring, pinCode, config.username)) { + logger.warn("Failed to initialize bridge, please check your credentials!"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_REGISTERING_ERROR, + "Failed to login to Verisure, please check your credentials!"); + return; + } + startAutomaticRefresh(); + } + }); + } catch (RuntimeException | UnsupportedEncodingException e) { + logger.warn("Failed to initialize: {}", e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + } + + @Override + public void dispose() { + logger.debug("Handler disposed."); + stopAutomaticRefresh(); + stopImmediateRefresh(); + session = null; + } + + public boolean registerObjectStatusListener( + DeviceStatusListener deviceStatusListener) { + VerisureSession mySession = session; + if (mySession != null) { + logger.debug("registerObjectStatusListener for listener {}", deviceStatusListener); + return mySession.registerDeviceStatusListener(deviceStatusListener); + } + return false; + } + + public boolean unregisterObjectStatusListener( + DeviceStatusListener deviceStatusListener) { + VerisureSession mySession = session; + if (mySession != null) { + logger.debug("unregisterObjectStatusListener for listener {}", deviceStatusListener); + return mySession.unregisterDeviceStatusListener(deviceStatusListener); + } + return false; + } + + @Override + public Collection> getServices() { + return Collections.singleton(VerisureThingDiscoveryService.class); + } + + private void refreshAndUpdateStatus() { + logger.debug("Refresh and update status!"); + VerisureSession session = this.session; + if (session != null) { + boolean success = session.refresh(); + if (success) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + } + } + } + + void scheduleImmediateRefresh(int refreshDelay) { + logger.debug("VerisureBridgeHandler - scheduleImmediateRefresh"); + immediateRefreshJobLock.lock(); + ScheduledFuture refreshJob = this.refreshJob; + ScheduledFuture immediateRefreshJob = this.immediateRefreshJob; + try { + // We schedule in 10 sec, to avoid multiple updates + if (refreshJob != null) { + logger.debug("Current remaining delay {} for refresh job {}", refreshJob.getDelay(TimeUnit.SECONDS), + refreshJob); + if (immediateRefreshJob != null) { + logger.debug("Current remaining delay {} for immediate refresh job {}", + immediateRefreshJob.getDelay(TimeUnit.SECONDS), immediateRefreshJob); + } + + if (refreshJob.getDelay(TimeUnit.SECONDS) > refreshDelay) { + if (immediateRefreshJob == null || immediateRefreshJob.getDelay(TimeUnit.SECONDS) <= 0) { + if (immediateRefreshJob != null) { + logger.debug("Current remaining delay {} for immediate refresh job {}", + immediateRefreshJob.getDelay(TimeUnit.SECONDS), immediateRefreshJob); + } + // Note we are using getDelay() instead of isDone() as we want to allow Things to schedule a + // refresh if their status is pending. As the status update happens inside the + // refreshAndUpdateStatus + // execution the isDone() will return false and would not allow the rescheduling of the task. + this.immediateRefreshJob = scheduler.schedule(this::refreshAndUpdateStatus, refreshDelay, + TimeUnit.SECONDS); + logger.debug("Scheduling new immediate refresh job {}", immediateRefreshJob); + } + } + } + } catch (RejectedExecutionException e) { + logger.warn("Immediate refresh job cannot be scheduled!"); + } finally { + immediateRefreshJobLock.unlock(); + } + } + + private void startAutomaticRefresh() { + ScheduledFuture refreshJob = this.refreshJob; + logger.debug("Start automatic refresh {}", refreshJob); + if (refreshJob == null || refreshJob.isCancelled()) { + try { + this.refreshJob = scheduler.scheduleWithFixedDelay(this::refreshAndUpdateStatus, 0, REFRESH_SEC, + TimeUnit.SECONDS); + logger.debug("Scheduling at fixed delay refreshjob {}", this.refreshJob); + } catch (RejectedExecutionException e) { + logger.warn("Automatic refresh job cannot be started!"); + } + } + } + + private void stopAutomaticRefresh() { + ScheduledFuture refreshJob = this.refreshJob; + logger.debug("Stop automatic refresh for job {}", refreshJob); + if (refreshJob != null) { + refreshJob.cancel(true); + this.refreshJob = null; + } + } + + private void stopImmediateRefresh() { + immediateRefreshJobLock.lock(); + ScheduledFuture immediateRefreshJob = this.immediateRefreshJob; + try { + logger.debug("Stop immediate refresh for job {}", immediateRefreshJob); + if (immediateRefreshJob != null) { + immediateRefreshJob.cancel(true); + this.immediateRefreshJob = null; + } + } catch (RejectedExecutionException e) { + logger.warn("Immediate refresh job cannot be scheduled!"); + } finally { + immediateRefreshJobLock.unlock(); + } + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureBroadbandConnectionThingHandler.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureBroadbandConnectionThingHandler.java new file mode 100644 index 0000000000000..ed001e9a4531e --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureBroadbandConnectionThingHandler.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.handler; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*; + +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.openhab.binding.verisure.internal.dto.VerisureBroadbandConnectionsDTO; + +/** + * Handler for the Broadband COnnection thing type that Verisure provides. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureBroadbandConnectionThingHandler extends VerisureThingHandler { + + public static final Set SUPPORTED_THING_TYPES = Collections + .singleton(THING_TYPE_BROADBAND_CONNECTION); + + public VerisureBroadbandConnectionThingHandler(Thing thing) { + super(thing); + } + + @Override + public Class getVerisureThingClass() { + return VerisureBroadbandConnectionsDTO.class; + } + + @Override + public synchronized void update(VerisureBroadbandConnectionsDTO thing) { + updateBroadbandConnection(thing); + updateStatus(ThingStatus.ONLINE); + } + + private void updateBroadbandConnection(VerisureBroadbandConnectionsDTO vbcJSON) { + String testDate = vbcJSON.getData().getInstallation().getBroadband().getTestDate(); + if (testDate != null) { + updateTimeStamp(testDate); + ChannelUID cuid = new ChannelUID(getThing().getUID(), CHANNEL_CONNECTED); + boolean broadbandConnected = vbcJSON.getData().getInstallation().getBroadband().isBroadbandConnected(); + updateState(cuid, OnOffType.from(broadbandConnected)); + updateInstallationChannels(vbcJSON); + } + } + + @Override + public void updateTriggerChannel(String event) { + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureClimateDeviceThingHandler.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureClimateDeviceThingHandler.java new file mode 100644 index 0000000000000..4826fd523a971 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureClimateDeviceThingHandler.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.handler; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*; + +import java.util.HashSet; +import java.util.Set; + +import javax.measure.quantity.Dimensionless; +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.verisure.internal.dto.VerisureClimatesDTO; + +/** + * Handler for all Climate Device thing types that Verisure provides. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureClimateDeviceThingHandler extends VerisureThingHandler { + + public static final Set SUPPORTED_THING_TYPES = new HashSet(); + static { + SUPPORTED_THING_TYPES.add(THING_TYPE_SMOKEDETECTOR); + SUPPORTED_THING_TYPES.add(THING_TYPE_WATERDETECTOR); + SUPPORTED_THING_TYPES.add(THING_TYPE_SIREN); + SUPPORTED_THING_TYPES.add(THING_TYPE_NIGHT_CONTROL); + } + + public VerisureClimateDeviceThingHandler(Thing thing) { + super(thing); + } + + @Override + public Class getVerisureThingClass() { + return VerisureClimatesDTO.class; + } + + @Override + public synchronized void update(VerisureClimatesDTO thing) { + updateClimateDeviceState(thing); + updateStatus(ThingStatus.ONLINE); + } + + private void updateClimateDeviceState(VerisureClimatesDTO climateJSON) { + getThing().getChannels().stream().map(Channel::getUID) + .filter(channelUID -> isLinked(channelUID) && !channelUID.getId().equals("timestamp")) + .forEach(channelUID -> { + State state = getValue(channelUID.getId(), climateJSON); + updateState(channelUID, state); + }); + String timeStamp = climateJSON.getData().getInstallation().getClimates().get(0).getTemperatureTimestamp(); + if (timeStamp != null) { + updateTimeStamp(timeStamp); + } + updateInstallationChannels(climateJSON); + } + + public State getValue(String channelId, VerisureClimatesDTO climateJSON) { + switch (channelId) { + case CHANNEL_TEMPERATURE: + double temperature = climateJSON.getData().getInstallation().getClimates().get(0).getTemperatureValue(); + return new QuantityType(temperature, SIUnits.CELSIUS); + case CHANNEL_HUMIDITY: + if (climateJSON.getData().getInstallation().getClimates().get(0).isHumidityEnabled()) { + double humidity = climateJSON.getData().getInstallation().getClimates().get(0).getHumidityValue(); + return new QuantityType(humidity, SmartHomeUnits.PERCENT); + } + case CHANNEL_HUMIDITY_ENABLED: + boolean humidityEnabled = climateJSON.getData().getInstallation().getClimates().get(0) + .isHumidityEnabled(); + return OnOffType.from(humidityEnabled); + case CHANNEL_LOCATION: + String location = climateJSON.getLocation(); + return location != null ? new StringType(location) : UnDefType.NULL; + } + return UnDefType.UNDEF; + } + + @Override + public void updateTriggerChannel(String event) { + logger.debug("ClimateThingHandler trigger event {}", event); + triggerChannel(CHANNEL_SMOKE_DETECTION_TRIGGER_CHANNEL, event); + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureDoorWindowThingHandler.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureDoorWindowThingHandler.java new file mode 100644 index 0000000000000..26a56be7c8265 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureDoorWindowThingHandler.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.handler; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OpenClosedType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.verisure.internal.dto.VerisureDoorWindowsDTO; +import org.openhab.binding.verisure.internal.dto.VerisureDoorWindowsDTO.DoorWindow; + +/** + * Handler for the Smart Lock Device thing type that Verisure provides. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureDoorWindowThingHandler extends VerisureThingHandler { + + public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_DOORWINDOW); + + public VerisureDoorWindowThingHandler(Thing thing) { + super(thing); + } + + @Override + public Class getVerisureThingClass() { + return VerisureDoorWindowsDTO.class; + } + + @Override + public synchronized void update(VerisureDoorWindowsDTO thing) { + updateDoorWindowState(thing); + updateStatus(ThingStatus.ONLINE); + } + + private void updateDoorWindowState(VerisureDoorWindowsDTO doorWindowJSON) { + List doorWindowList = doorWindowJSON.getData().getInstallation().getDoorWindows(); + if (!doorWindowList.isEmpty()) { + DoorWindow doorWindow = doorWindowList.get(0); + + getThing().getChannels().stream().map(Channel::getUID) + .filter(channelUID -> isLinked(channelUID) && !channelUID.getId().equals("timestamp")) + .forEach(channelUID -> { + State state = getValue(channelUID.getId(), doorWindow); + updateState(channelUID, state); + + }); + updateTimeStamp(doorWindow.getReportTime()); + updateInstallationChannels(doorWindowJSON); + } else { + logger.debug("DoorWindow list is empty!"); + } + } + + public State getValue(String channelId, DoorWindow doorWindow) { + switch (channelId) { + case CHANNEL_STATE: + return "OPEN".equals(doorWindow.getState()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED; + case CHANNEL_LOCATION: + String location = doorWindow.getDevice().getArea(); + return location != null ? new StringType(location) : UnDefType.UNDEF; + } + return UnDefType.UNDEF; + } + + @Override + public void updateTriggerChannel(String event) { + logger.debug("DoorWindowThingHandler trigger event {}", event); + triggerChannel(CHANNEL_DOOR_WINDOW_TRIGGER_CHANNEL, event); + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureEventLogThingHandler.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureEventLogThingHandler.java new file mode 100644 index 0000000000000..469be44b77836 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureEventLogThingHandler.java @@ -0,0 +1,174 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.handler; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.verisure.internal.VerisureSession; +import org.openhab.binding.verisure.internal.VerisureThingConfiguration; +import org.openhab.binding.verisure.internal.dto.VerisureBaseThingDTO.Device; +import org.openhab.binding.verisure.internal.dto.VerisureEventLogDTO; +import org.openhab.binding.verisure.internal.dto.VerisureEventLogDTO.EventLog; +import org.openhab.binding.verisure.internal.dto.VerisureEventLogDTO.PagedList; + +/** + * Handler for the Event Log thing type that Verisure provides. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureEventLogThingHandler extends VerisureThingHandler { + + private BigDecimal lastEventId = BigDecimal.ZERO; + private long lastEventTime = 0; + + public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_EVENT_LOG); + + public VerisureEventLogThingHandler(Thing thing) { + super(thing); + } + + @Override + public Class getVerisureThingClass() { + return VerisureEventLogDTO.class; + } + + @Override + public synchronized void update(VerisureEventLogDTO thing) { + updateEventLogState(thing); + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void initialize() { + logger.debug("initialize on thing: {}", thing); + VerisureSession session = getSession(); + config = getConfigAs(VerisureThingConfiguration.class); + if (session != null) { + logger.debug("Set number of events to fetch from API to {} for thing {}", config.getNumberOfEvents(), + thing); + session.setNumberOfEvents(config.getNumberOfEvents()); + } + super.initialize(); + } + + private void updateEventLogState(VerisureEventLogDTO eventLogJSON) { + EventLog eventLog = eventLogJSON.getData().getInstallation().getEventLog(); + if (eventLog.getPagedList().size() > 0) { + getThing().getChannels().stream().map(Channel::getUID).filter(channelUID -> isLinked(channelUID)) + .forEach(channelUID -> { + State state = getValue(channelUID.getId(), eventLogJSON, eventLog); + updateState(channelUID, state); + }); + updateInstallationChannels(eventLogJSON); + String eventTime = eventLogJSON.getData().getInstallation().getEventLog().getPagedList().get(0) + .getEventTime(); + if (eventTime != null) { + updateTimeStamp(eventTime, CHANNEL_LAST_EVENT_TIME); + lastEventTime = ZonedDateTime.parse(eventTime).toEpochSecond(); + } + } else { + logger.debug("Empty event log."); + } + } + + public State getValue(String channelId, VerisureEventLogDTO verisureEventLog, EventLog eventLog) { + Device device = eventLog.getPagedList().get(0).getDevice(); + + switch (channelId) { + case CHANNEL_LAST_EVENT_LOCATION: + return device != null && device.getArea() != null ? new StringType(device.getArea()) : UnDefType.NULL; + case CHANNEL_LAST_EVENT_DEVICE_ID: + return device != null && device.getDeviceLabel() != null ? new StringType(device.getDeviceLabel()) + : UnDefType.NULL; + case CHANNEL_LAST_EVENT_ID: + String eventId = eventLog.getPagedList().get(0).getEventId(); + if (eventId != null) { + if (eventId.contains("-")) { + eventId = eventId.replace("-", ""); + lastEventId = new BigDecimal(new BigInteger(eventId, 16)); + } else { + lastEventId = new BigDecimal(eventId); + } + return new DecimalType(lastEventId); + } else { + return UnDefType.NULL; + } + case CHANNEL_LAST_EVENT_TIME: + if (lastEventTime != 0) { + triggerEventChannels(eventLog); + } + case CHANNEL_LAST_EVENT_DEVICE_TYPE: + return device != null && device.getGui().getLabel() != null ? new StringType(device.getGui().getLabel()) + : UnDefType.NULL; + case CHANNEL_LAST_EVENT_TYPE: + String lastEventType = eventLog.getPagedList().get(0).getEventType(); + return lastEventType != null ? new StringType(lastEventType) : UnDefType.NULL; + case CHANNEL_LAST_EVENT_CATEGORY: + String lastEventCategory = eventLog.getPagedList().get(0).getEventCategory(); + return lastEventCategory != null ? new StringType(lastEventCategory) : UnDefType.NULL; + case CHANNEL_LAST_EVENT_USER_NAME: + String lastEventUserName = eventLog.getPagedList().get(0).getUserName(); + return lastEventUserName != null ? new StringType(lastEventUserName) : UnDefType.NULL; + case CHANNEL_EVENT_LOG: + String eventLogJSON = gson.toJson(eventLog); + return eventLogJSON != null ? new StringType(eventLogJSON) : UnDefType.NULL; + } + return UnDefType.UNDEF; + } + + private void triggerEventChannels(EventLog eventLog) { + List newEventList = eventLog.getPagedList().stream().collect(Collectors.toList()); + Collections.reverse(newEventList); + ArrayList events = new ArrayList<>(); + for (PagedList newEvent : newEventList) { + long eventTime = ZonedDateTime.parse(newEvent.getEventTime()).toEpochSecond(); + logger.trace("Event time: {} Last Event time: {}", eventTime, lastEventTime); + if (eventTime > lastEventTime) { + logger.debug("Create event {} for event time {}", newEvent.getEventType(), eventTime); + Event event; + Device device = newEvent.getDevice(); + if (device != null) { + event = new Event(device.getDeviceLabel(), newEvent.getEventType(), newEvent.getEventCategory()); + } else { + event = new Event("NA", newEvent.getEventType(), newEvent.getEventCategory()); + } + events.add(event); + } + } + updateTriggerChannel(events); + } + + @Override + public void updateTriggerChannel(String event) { + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureGatewayThingHandler.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureGatewayThingHandler.java new file mode 100644 index 0000000000000..d8abe38ed36b9 --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureGatewayThingHandler.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.handler; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.verisure.internal.dto.VerisureGatewayDTO; +import org.openhab.binding.verisure.internal.dto.VerisureGatewayDTO.CommunicationState; + +/** + * Handler for the Gateway thing type that Verisure provides. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureGatewayThingHandler extends VerisureThingHandler { + + public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_GATEWAY); + + public VerisureGatewayThingHandler(Thing thing) { + super(thing); + } + + @Override + public Class getVerisureThingClass() { + return VerisureGatewayDTO.class; + } + + @Override + public synchronized void update(VerisureGatewayDTO thing) { + updateGatewayState(thing); + updateStatus(ThingStatus.ONLINE); + } + + private void updateGatewayState(VerisureGatewayDTO gatewayJSON) { + List communicationStateList = gatewayJSON.getData().getInstallation() + .getCommunicationState(); + if (!communicationStateList.isEmpty()) { + communicationStateList.forEach(communicationState -> { + getThing().getChannels().stream().map(Channel::getUID).filter(channelUID -> isLinked(channelUID)) + .forEach(channelUID -> { + if (!channelUID.getId().contains("testTime")) { + State state = getValue(channelUID.getId(), gatewayJSON, communicationState); + updateState(channelUID, state); + } else { + String timestamp = communicationState.getTestDate(); + if (timestamp != null && channelUID.toString() + .contains(communicationState.getHardwareCarrierType())) { + updateTimeStamp(timestamp, channelUID); + } + } + }); + }); + updateInstallationChannels(gatewayJSON); + } else { + logger.debug("Empty communication state list."); + } + } + + public State getValue(String channelId, VerisureGatewayDTO verisureGateway, CommunicationState communicationState) { + switch (channelId) { + case CHANNEL_STATUS_GSM_OVER_UDP: + case CHANNEL_STATUS_GSM_OVER_SMS: + case CHANNEL_STATUS_GPRS_OVER_UDP: + case CHANNEL_STATUS_ETH_OVER_UDP: + String state = communicationState.getResult(); + return state != null ? new StringType(state) : UnDefType.NULL; + case CHANNEL_GATEWAY_MODEL: + String model = communicationState.getDevice().getGui().getLabel(); + return model != null ? new StringType(model) : UnDefType.NULL; + case CHANNEL_LOCATION: + String location = communicationState.getDevice().getArea(); + return location != null ? new StringType(location) : UnDefType.NULL; + } + return UnDefType.UNDEF; + } + + @Override + public void updateTriggerChannel(String event) { + logger.debug("GatewayThingHandler trigger event {}", event); + triggerChannel(CHANNEL_GATEWAY_TRIGGER_CHANNEL, event); + } +} diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureMiceDetectionThingHandler.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureMiceDetectionThingHandler.java new file mode 100644 index 0000000000000..31067bce1092c --- /dev/null +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureMiceDetectionThingHandler.java @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.verisure.internal.handler; + +import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import javax.measure.quantity.Temperature; +import javax.measure.quantity.Time; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.verisure.internal.dto.VerisureMiceDetectionDTO; +import org.openhab.binding.verisure.internal.dto.VerisureMiceDetectionDTO.Detection; +import org.openhab.binding.verisure.internal.dto.VerisureMiceDetectionDTO.Mouse; + +/** + * Handler for the Mice Detection thing type that Verisure provides. + * + * @author Jan Gustafsson - Initial contribution + * + */ +@NonNullByDefault +public class VerisureMiceDetectionThingHandler extends VerisureThingHandler { + + public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MICE_DETECTION); + + public VerisureMiceDetectionThingHandler(Thing thing) { + super(thing); + } + + @Override + public Class getVerisureThingClass() { + return VerisureMiceDetectionDTO.class; + } + + @Override + public synchronized void update(VerisureMiceDetectionDTO thing) { + updateMiceDetectionState(thing); + updateStatus(ThingStatus.ONLINE); + } + + private void updateMiceDetectionState(VerisureMiceDetectionDTO miceDetectionJSON) { + List miceList = miceDetectionJSON.getData().getInstallation().getMice(); + if (!miceList.isEmpty()) { + Mouse mouse = miceList.get(0); + getThing().getChannels().stream().map(Channel::getUID).filter(channelUID -> isLinked(channelUID) + && !channelUID.getId().equals("timestamp") && !channelUID.getId().equals("temperatureTimestamp")) + .forEach(channelUID -> { + State state = getValue(channelUID.getId(), miceDetectionJSON, mouse); + updateState(channelUID, state); + }); + if (mouse.getDetections().size() != 0) { + updateTimeStamp(mouse.getDetections().get(0).getNodeTime()); + } + updateTimeStamp(miceDetectionJSON.getTemperatureTime(), CHANNEL_TEMPERATURE_TIMESTAMP); + updateInstallationChannels(miceDetectionJSON); + } else { + logger.debug("MiceList is empty!"); + } + } + + public State getValue(String channelId, VerisureMiceDetectionDTO miceDetectionJSON, Mouse mouse) { + switch (channelId) { + case CHANNEL_COUNT_LATEST_DETECTION: + if (mouse.getDetections().size() == 0) { + return new DecimalType(0); + } else { + return new DecimalType(mouse.getDetections().get(0).getCount()); + } + case CHANNEL_COUNT_LAST_24_HOURS: + if (mouse.getDetections().size() == 0) { + return new DecimalType(0); + } else { + return new DecimalType(mouse.getDetections().stream().mapToInt(Detection::getCount).sum()); + } + case CHANNEL_DURATION_LATEST_DETECTION: + if (mouse.getDetections().size() == 0) { + return new QuantityType