added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.homematic</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,14 @@
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

View File

@@ -0,0 +1,681 @@
# Homematic Binding
This is the binding for the [eQ-3 Homematic Solution](https://eq-3.de/).
This binding allows you to integrate, view, control and configure all Homematic devices in openHAB.
## Configuration of the CCU
Under `Home page > Settings > Control panel` with the menu `Configure Firewall` the Firewall configurations have to be adjusted.
The CCU has to be configured to have "XML-RPC" set to "Full Access" or "Restricted access".
Also the "Remote Homematic-Script API" has to be set to "Full Access" or "Restricted access".
When the option "Restricted access" is used, some ports have to be added to the "Port opening" list.
```
2000;
2001;
2010;
8701;
9292;
```
Also the IP of the device running openHAB has to be set to the list of "IP addresses for restricted access".
Also under `Home page > Settings > Control panel` with the menu `Security` the option `Authentication` has to be disabled as the binding does not support the configuration of `username` and `password`for the XML-RPC API.
If this is not done the binding will not be able to connect to the CCU and the CCU Thing will stay uninitialized and sets a timeout exception:
```
xxx-xx-xx xx:xx:xx.xxx [hingStatusInfoChangedEvent] - - 'homematic:bridge:xxx' changed from INITIALIZING to OFFLINE (COMMUNICATION_ERROR): java.net.SocketTimeoutException: Connect Timeout
```
## Supported Bridges
All gateways which provides the Homematic BIN- or XML-RPC API:
- CCU 1, 2 and 3
- [RaspberryMatic](https://github.com/jens-maus/RaspberryMatic)
- [Homegear](https://homegear.eu) (>= 0.8.0-1988)
- [piVCCU](https://github.com/alexreinert/piVCCU)
- [YAHM](https://github.com/leonsio/YAHM)
- [Windows BidCos service](https://eq-3.de/service/downloads.html?kat=download&id=125) (included in "LAN Usersoftware" download)
- [OCCU](https://github.com/eq-3/occu)
The Homematic IP Access Point **does not support** this API and and can't be used with this binding.
Homematic IP support:
- CCU2 with at least firmware 2.17.15
- [RaspberryMatic](https://github.com/jens-maus/RaspberryMatic) with the [HM-MOD-RPI-PCB](https://www.elv.de/homematic-funkmodul-fuer-raspberry-pi-bausatz.html) or [RPI-RF-MOD](https://www.elv.de/homematic-funk-modulplatine-fuer-raspberry-pi-3-rpi-rf-mod-komplettbausatz.html) RF module
- [piVCCU](https://github.com/alexreinert/piVCCU)
- [YAHM](https://github.com/leonsio/YAHM)
These ports are used by the binding by default to communicate **TO** the gateway:
- RF components: 2001
- WIRED components: 2000
- HMIP components: 2010
- CUxD: 8701
- TclRegaScript: 8181
- Groups: 9292
And **FROM** the gateway to the binding:
- XML-RPC: 9125
- BIN-RPC: 9126
CCU Autodiscovery:
- UDP 43439
**Note:** The binding tries to identify the gateway with XML-RPC and uses henceforth:
- **CCU**
- **RF**: XML-RPC
- **WIRED**: XML-RPC
- **HMIP**: XML-RPC
- **CUxD**: BIN-RPC (CUxD version >= 1.6 required)
- **Groups**: XML-RPC
- **Homegear**
- BIN-RPC
- **Other**
- XML-RPC
## Supported Things
All devices connected to a Homematic gateway.
All required metadata are generated during device discovery.
With Homegear or a CCU, variables and scripts are supported too.
## Discovery
Gateway discovery is available:
- CCU
- RaspberryMatic >= 2.29.23.20171022
- Homegear >= 0.6.x
- piVCCU
For all other gateways you have to manually add a bridge in a things file. Device discovery is supported for all gateways.
The binding has a gateway type autodetection, but sometimes a gateway does not clearly notify the type.
If you are using a YAHM for example, you have to manually set the gateway type in the bride configuration to CCU.
If autodetection can not identify the gateway, the binding uses the default gateway implementation.
The difference is, that variables, scripts and device names are not supported, everything else is the same.
### Automatic install mode during discovery
Besides discovering devices that are already known by the gateway, it may be desired to connect new devices to your system - which requires your gateway to be in install mode.
Starting the binding's DiscoveryService will automatically put your gateway(s) in install mode for a specified period of time (see installModeDuration).
**Note:** Enabling / disabling of install mode is also available via GATEWAY-EXTRAS.
You may use this if you prefer.
**Exception:** If a gateway is not ONLINE, the install mode will not be set automatically.
For instance during initialization of the binding its DiscoveryService is started and will discover devices that are already connected.
However, the install mode is not automatically enabled in this situation because the gateway is in the status INITIALIZING.
## Bridge Configuration
There are several settings for a bridge:
- **gatewayAddress** (required)
Network address of the Homematic gateway
- **gatewayType**
Hint for the binding to identify the gateway type (auto|ccu|noccu) (default = "auto").
- **callbackHost**
Callback network address of the system runtime, default is auto-discovery
- **bindAddress**
The address the XML-/BINRPC server binds to, default is value of "callbackHost"
- **callbackPort** (DEPRECATED, use "binCallbackPort" resp. "xmlCallbackPort")
Callback port of the binding's server, default is 9125 and counts up for each additional bridge
- **xmlCallbackPort**
Callback port of the binding's XML-RPC server, default is 9125 and counts up for each additional bridge
- **binCallbackPort**
Callback port of the binding's BIN-RPC server, default is 9126 and counts up for each additional bridge
- **aliveInterval** (DEPRECATED, not necessary anymore)
The interval in seconds to check if the communication with the Homematic gateway is still alive. If no message receives from the Homematic gateway, the RPC server restarts (default = 300)
- **reconnectInterval** (DEPRECATED, not necessary anymore)
The interval in seconds to force a reconnect to the Homematic gateway, disables "aliveInterval"! (0 = disabled, default = disabled).
If you have no sensors which sends messages in regular intervals and/or you have low communication, the "aliveInterval" may restart the connection to the Homematic gateway to often.
The "reconnectInterval" disables the "aliveInterval" and reconnects after a fixed period of time.
Think in hours when configuring (one hour = 3600)
- **timeout**
The timeout in seconds for connections to a Homematic gateway (default = 15)
- **discoveryTimeToLive**
The time to live in seconds for discovery results of a Homematic gateway (default = -1, which means infinite)
- **socketMaxAlive**
The maximum lifetime of a socket connection to and from a Homematic gateway in seconds (default = 900)
- **rfPort**
The port number of the RF daemon (default = 2001)
- **wiredPort**
The port number of the HS485 daemon (default = 2000)
- **hmIpPort**
The port number of the HMIP server (default = 2010)
- **cuxdPort**
The port number of the CUxD daemon (default = 8701)
- **installModeDuration**
Time in seconds that the controller will be in install mode when a device discovery is initiated (default = 60)
- **unpairOnDeletion**
If set to true, devices are automatically unpaired from the gateway when their corresponding things are deleted.
**Warning:** The option "factoryResetOnDeletion" also unpairs a device, so in order to avoid unpairing on deletion completely, both options need to be set to false! (default = false)
- **factoryResetOnDeletion**
If set to true, devices are automatically factory reset when their corresponding things are removed.
Due to the factory reset, the device will also be unpaired from the gateway, even if "unpairOnDeletion" is set to false! (default = false)
The syntax for a bridge is:
```java
homematic:bridge:NAME
```
- **homematic** the binding id, fixed
- **bridge** the type, fixed
- **name** the name of the bridge
### Example
**Minimum configuration**
```java
Bridge homematic:bridge:ccu [ gatewayAddress="..."]
```
**With callback settings**
```java
Bridge homematic:bridge:ccu [ gatewayAddress="...", callbackHost="...", callbackPort=... ]
```
**Multiple bridges**
```java
Bridge homematic:bridge:lxccu [ gatewayAddress="..."]
Bridge homematic:bridge:occu [ gatewayAddress="..."]
```
## Thing Configuration
Things are all discovered automatically, you can handle them in PaperUI.
If you really like to manually configure a thing:
```java
Bridge homematic:bridge:ccu [ gatewayAddress="..." ]
{
Thing HM-LC-Dim1T-Pl-2 JEQ0999999
}
```
The first parameter after Thing is the device type, the second the serial number.
If you are using Homegear, you have to add the prefix `HG-` for each type.
The `HG-` prefix is only needed for Things, not for Items or channel configs.
This is necessary, because the Homegear devices supports more datapoints than Homematic devices.
```java
Thing HG-HM-LC-Dim1T-Pl-2 JEQ0999999
```
As additional parameters you can define a name and a location for each thing.
The `Name` will be used to identify the thing in the Paper UI lists, the `Location` will be used in the Control section of PaperUI to sort the things.
```java
Thing HG-HM-LC-Dim1T-Pl-2 JEQ0999999 "Name" @ "Location"
```
All channels have two configs:
- **delay**: delays transmission of a command **to** the Homematic gateway, duplicate commands are filtered out
- **receiveDelay**: delays a received event **from** the Homematic gateway, duplicate events are filtered out (OH 2.2)
The `receiveDelay` is handy for dimmers and roller shutters for example.
If you have a slider in a UI and you move this slider to a new position, it jumps around because the gateway sends multiple events with different positions until the final has been reached.
If you set the `receiveDelay` to some seconds, these events are filtered out and only the last position is distributed to the binding.
The disadvantage is of course, that all events for this channel are delayed.
```java
Thing HM-LC-Dim1T-Pl-2 JEQ0999999 "Name" @ "Location" {
Channels:
Type HM-LC-Dim1T-Pl-2_1_level : 1#LEVEL [
delay = 0,
receiveDelay = 4
]
}
```
The `Type` is the device type, channel number and lowercase channel name separated with an underscore.
Note that, for Homegear devices, in contrast to the specification of the Rhing above no `HG-` prefix is needed for the specification of the Type of the Channel.
The channel configs are optional.
Example without channel configs
```java
Thing HM-LC-Dim1T-Pl-2 JEQ0999999 "Name" @ "Location" {
Channels:
Type HM-LC-Dim1T-Pl-2_1_LEVEL : 1#LEVEL
}
```
### Items
In the items file, you can map the datapoints. The syntax is:
```java
homematic:TYPE:BRIDGE:SERIAL:CHANNELNUMBER#DATAPOINTNAME
```
- **homematic:** the binding id, fixed
- **type:** the type of the Homematic device
- **bridge:** the name of the bridge
- **serial:** the serial number of the Homematic device
- **channelnumber:** the channel number of the Homematic datapoint
- **datapointname:** the name of the Homematic datapoint
```java
Switch RC_1 "Remote Control Button 1" { channel="homematic:HM-RC-19-B:ccu:KEQ0099999:1#PRESS_SHORT" }
Dimmer Light "Light [%d %%]" { channel="homematic:HM-LC-Dim1T-Pl-2:ccu:JEQ0555555:1#LEVEL" }
```
**Note:** don't forget to add the `HG-` type prefix for Homegear devices
## Virtual device GATEWAY-EXTRAS
The GATEWAY-EXTRAS is a virtual device which contains a switch to reload all values from all devices and also a switch to put the gateway in the install mode to add new devices.
If the gateway supports variables and scripts, you can handle them with this device too.
The type is generated: `GATEWAY-EXTRAS-[BRIDGE_ID]`.
**Example:** bridgeId=ccu, type=GATEWAY-EXTRAS-CCU
Address: fixed GWE00000000
### RELOAD_ALL_FROM_GATEWAY
A virtual datapoint (Switch) to reload all values for all devices, available in channel 0 in GATEWAY-EXTRAS
### RELOAD_RSSI
A virtual datapoint (Switch) to reload all RSSI values for all devices, available in channel 0 in GATEWAY-EXTRAS
### INSTALL_MODE
A virtual datapoint (Switch) to start the install mode on the gateway, available in channel 0 in GATEWAY-EXTRAS
### INSTALL_MODE_DURATION
A virtual datapoint (Integer) to hold the duration for the install mode, available in channel 0 in GATEWAY-EXTRAS (max 300 seconds, default = 60)
## Virtual datapoints
Virtual datapoints are generated by the binding and provide special functionality for several device types.
### RSSI
A virtual datapoint (Number) with the unified RSSI value from RSSI_DEVICE and RSSI_PEER, available in channel 0 for all wireless devices
### DELETE_MODE
A virtual datapoint (Switch) to remove the device from the gateway, available in channel 0 for each device. Deleting a device is only possible if DELETE_DEVICE_MODE is not LOCKED
### DELETE_DEVICE_MODE
A virtual datapoint (Enum) to configure the device deletion with DELETE_MODE, available in channel 0 for each device
- **LOCKED:** (default) device can not be deleted
- **RESET:** device is reset to factory settings before deleting
- **FORCE:** device is also deleted if it is not reachable
- **DEFER:** if the device can not be reached, it is deleted at the next opportunity
**Note:** if you change the value and don't delete the device, the virtual datapoints resets to LOCKED after 30 seconds
### ON_TIME_AUTOMATIC
A virtual datapoint (Number) to automatically set the ON_TIME datapoint before the STATE or LEVEL datapoint is sent to the gateway, available for all devices which supports the ON_TIME datapoint.
This is useful to automatically turn off the datapoint after the specified time.
### BUTTON
A virtual datapoint (String) to simulate a key press, available on all channels that contains PRESS_ datapoints.
Available values:
- `SHORT_PRESS`: triggered on a short key press
- `LONG_PRESS`: triggered on a key press longer than `LONG_PRESS_TIME` (variable configuration per key, default is 0.4 s)
- `DOUBLE_PRESS`: triggered on a short key press but only if the latest `SHORT_PRESS` or `DOUBLE_PRESS` event is not older than 2.0 s (not related to `DBL_PRESS_TIME` configuration, which is more like a key lock because if it is other than `0.0` single presses are not notified anymore)
**Example:** to capture a short key press on the 19 button remote control in a rule
```javascript
rule "example trigger rule"
when
Channel 'homematic:HM-RC-19-B:ccu:KEQ0012345:1#BUTTON' triggered SHORT_PRESS
then
...
end
```
### DISPLAY_OPTIONS (only HM-RC-19)
A virtual datapoint (String) to control the display of a 19 button Homematic remote control (HM-RC-19), available on channel 18
The remote control display is limited to five characters, a longer text is truncated.
You have several additional options to control the display.
- BEEP *(TONE1, TONE2, TONE3)* - let the remote control beep
- BACKLIGHT *(BACKLIGHT_ON, BLINK_SLOW, BLINK_FAST)* - control the display backlight
- UNIT *(PERCENT, WATT, CELSIUS, FAHRENHEIT)* - display one of these units
- SYMBOL *(BULB, SWITCH, WINDOW, DOOR, BLIND, SCENE, PHONE, BELL, CLOCK, ARROW_UP, ARROW_DOWN)* - display symbols, multiple symbols possible
You can combine any option, they must be separated by a comma.
If you specify more than one option for BEEP, BACKLIGHT and UNIT, only the first one is taken into account and all others are ignored. For SYMBOL you can specify multiple options.
**Examples:**
Assumed you mapped the virtual datapoint to a String item called `Display_Options`.
```java
String Display_Options "Display_Options" { channel="homematic:HM-RC-19-B:ccu:KEQ0099999:18#DISPLAY_OPTIONS" }
```
show message "TEST":
```shell
smarthome send Display_Options "TEST"
```
show message "TEXT", beep once and turn backlight on:
```shell
smarthome send Display_Options "TEXT, TONE1, BACKLIGHT_ON"
```
show message "15", beep once, turn backlight on and shows the celsius unit:
```shell
smarthome send Display_Options "15, TONE1, BACKLIGHT_ON, CELSIUS"
```
show message "ALARM", beep three times, let the backlight blink fast and shows a bell symbol:
```shell
smarthome send Display_Options "ALARM, TONE3, BLINK_FAST, BELL"
```
Duplicate options: TONE3 is ignored, because TONE1 is specified previously.
```shell
smarthome send Display_Options "TEXT, TONE1, BLINK_FAST, TONE3"
```
### DISPLAY_SUBMIT (only HM-Dis-WM55 and HM-Dis-EP-WM55)
Adds multiple virtual datapoints to the HM-Dis-WM55 and HM-Dis-EP-WM55 devices to easily send (colored) text and icons to the display.
**Note:** The HM-Dis-EP-WM55 has only a black and white display and therefore does not support datapoints for colored lines. In addition, only lines 1-3 can be set.
#### Example ####
Display text at line 1,3 and 5 when the bottom button on the display is pressed
**Items**
```java
String Display_line_1 "Line 1" { channel="homematic:HM-Dis-WM55:ccu:NEQ0123456:1#DISPLAY_LINE_1" }
String Display_line_3 "Line 3" { channel="homematic:HM-Dis-WM55:ccu:NEQ0123456:1#DISPLAY_LINE_3" }
String Display_line_5 "Line 5" { channel="homematic:HM-Dis-WM55:ccu:NEQ0123456:1#DISPLAY_LINE_5" }
String Display_color_1 "Color 1" { channel="homematic:HM-Dis-WM55:ccu:NEQ0123456:1#DISPLAY_COLOR_1" }
String Display_color_3 "Color 3" { channel="homematic:HM-Dis-WM55:ccu:NEQ0123456:1#DISPLAY_COLOR_3" }
String Display_color_5 "Color 5" { channel="homematic:HM-Dis-WM55:ccu:NEQ0123456:1#DISPLAY_COLOR_5" }
String Display_icon_1 "Icon 1" { channel="homematic:HM-Dis-WM55:ccu:NEQ0123456:1#DISPLAY_ICON_1" }
String Display_icon_3 "Icon 3" { channel="homematic:HM-Dis-WM55:ccu:NEQ0123456:1#DISPLAY_ICON_3" }
String Display_icon_5 "Icon 5" { channel="homematic:HM-Dis-WM55:ccu:NEQ0123456:1#DISPLAY_ICON_5" }
Switch Button_bottom "Button" { channel="homematic:HM-Dis-WM55:ccu:NEQ0123456:1#PRESS_SHORT" }
Switch Display_submit "Submit" { channel="homematic:HM-Dis-WM55:ccu:NEQ0123456:1#DISPLAY_SUBMIT" }
```
**Rule**
```javascript
rule "Display Test"
when
Item Button_bottom received update ON
then
Display_line_1.sendCommand("Line 1")
Display_line_3.sendCommand("Line 3")
Display_line_5.sendCommand("Line 5")
Display_icon_1.sendCommand("NONE")
Display_icon_3.sendCommand("OPEN")
Display_icon_5.sendCommand("INFO")
Display_color_1.sendCommand("NONE")
Display_color_3.sendCommand("RED")
Display_color_5.sendCommand("BLUE")
Display_submit.sendCommand(ON)
end
```
### HmIP-WRCD
The HmIP-WRCD display lines can be set via a combined parameter:
```java
String Display_CombinedParam "Combined Parameter" {channel="homematic:HmIP-WRCD:ccu:123456:3#COMBINED_PARAMETER"}
```
#### Set Display Lines
The combined parameter can be used in a rule file like this:
```java
Display_CombinedParam.sendCommand("{DDBC=WHITE,DDTC=BLACK,DDI=0,DDA=CENTER,DDS=Just a test,DDID=3,DDC=true}")
```
If you want to use the combined parameter in the console, you have to use ' instead of ", to prevent evaluation of curly braces:
```shell
smarthome:send Display_CombinedParam '{DDBC=WHITE,DDTC=BLACK,DDI=0,DDA=CENTER,DDS=Just a test,DDID=3,DDC=true}'
```
**Key translation:**
- DDBC: Background color of this line. (*WHITE*, *BLACK*)
- DDTC: Text color of this line. (*WHITE*, *BLACK*)
- DDI: Icon to be shown after text. (see icon listing below)
- DDA: Alignment of this line. (*LEFT*, *CENTER*, *RIGHT*)
- DDS: Text of this line. (String, but see special character listing below)
- DDID: Line number. (*1-5*)
- DDC: Commit, should be set in the last line, otherwise leave unset. (*true*)
Each line can be updated separately without changing the other lines.
Multiple lines can be updated within one command, use comma to separate each line.
Here an example for a rule file:
```java
Display_CombinedParam.sendCommand("{DDBC=WHITE,DDTC=BLACK,DDI=24,DDA=LEFT,DDS=Window open,DDID=4},{DDBC=WHITE,DDTC=BLACK,DDI=0,DDA=LEFT,DDS=Temp.: %sC,DDID=2,DDC=true}")
```
**Special Characters:**
- [ -> Ä
- \# -> Ö
- $ -> Ü
- { -> ä
- | -> ö
- } -> ü
- _ -> ß
- ] -> &
- ' -> =
- ; -> Sand Glass
- < -> Arrow Down
- = -> Arrow Up
- \> -> Arrow Up Right
- @ -> Arrow Down Right
**Icons:**
- 0 - No Icon
- 1 - Light off
- 2 - Light on
- 3 - Locked
- 4 - Unlocked
- 5 - X
- 6 - Check
- 7 - Information
- 8 - Envelope
- 9 - Spanner
- 10 - Sun
- 11 - Moon
- 12 - Wind
- 13 - Cloud
- 14 - Cloud/Lightning
- 15 - Cloud/Light Rain
- 16 - Cloud/Moon
- 17 - Cloud/Rain
- 18 - Cloud/Snow
- 19 - Cloud/Sun
- 20 - Cloud/Sun/Rain
- 21 - Cloud/Snowflake
- 22 - Cloud/Raindrop
- 23 - Flame
- 24 - Window Open
- 25 - Roller Shutter
- 26 - Eco
- 27 - ? (Rectangle in circle)
- 28 - House with person
- 29 - House empty
- 30 - Bell
- 31 - Clock
#### Alarm Beep
The display can also make short beep alarms:
```java
Display_CombinedParam.sendCommand("{R=0,IN=10,ANS=0}")
```
Note, that a commit (`DDC`) is not necessary for sounds.
As with line configuration, this can be combined with other line updates, separated with a comma.
**Key translations**
- R: Repetitions (*0 to 15*, 15=infinite)
- IN: Interval (*5 to 80* in steps of five)
- ANS: Beep sound (*-1 to 7*, see beep table)
**Beep Sounds**
This is the official mapping for the beep sounds
- -1 - No Sound
- 0 - Empty Battery
- 1 - Alarm Off
- 2 - External Alarm activated
- 3 - Internal Alarm activated
- 4 - External Alarm delayed activated
- 5 - Internal Alarm delayed activated
- 6 - Event
- 7 - Error
## Troubleshooting
**SHORT & LONG_PRESS events of push buttons do not occur on the event bus**
It seems buttons like the HM-PB-2-WM55 do just send these kind of events to the CCU if they are mentioned in a CCU program.
A simple workaround to make them send these events is, to create a program (rule inside the CCU) that does just have a "When" part and no "Then" part, in this "When" part each channel needs to be mentioned at least once.
As the HM-PB-2-WM55 for instance has two channels, it is enough to mention the SHORT_PRESS event of channel 1 & 2.
The LONG_PRESS events will work automatically as they are part of the same channels.
After the creation of this program, the button device will receive configuration data from the CCU which have to be accepted by pressing the config-button at the back of the device.
**INSTALL_TEST**
If a button is still not working and you do not see any PRESS_LONG / SHORT in your log file (log level DEBUG), it could be because of enabled security.
Try to disable security of your buttons in the HomeMatic Web GUI and try again.
If you can't disable security try to use key INSTALL_TEST which gets updated to ON for each key press
**-1 Failure**
A device may return this failure while fetching the datapoint values.
I have tested pretty much but I did not find the reason.
The HM-ES-TX-WM device for example always returns this failure, it is impossible with the current CCU2 firmware (2.17.15) to fetch the values.
I have implemented two workarounds, if a device returns the failure, workaround one is executed, if the device still returns the failure, workaround two is executed.
This always works in my tests, but you may see an OFFLINE, ONLINE cycle for the device.
Fetching values is only done at startup or if you trigger a REFRESH.
I hope this will be fixed in one of the next CCU firmwares.
With [Homegear](https://www.homegear.eu) everything works as expected.
**No variables and scripts in GATEWAY-EXTRAS**
The gateway autodetection of the binding can not clearly identify the gateway and falls back to the default implementation.
Use the ```gatewayType=ccu``` config to force the binding to use the CCU implementation.
**Variables out of sync**
The CCU only sends an event if a datapoint of a device has changed.
There is (currently) no way to receive an event automatically when a variable has changed.
To reload all variable values, send a REFRESH command to any variable.
E.g you have an item linked to a variable with the name `Var_1`.
In the console:
```shell
smarthome:send Var_1 REFRESH
```
In scripts:
```javascript
import org.openhab.core.types.RefreshType
...
Var_1.sendCommand(RefreshType.REFRESH)
```
**Note:** adding new and removing deleted variables from the GATEWAY-EXTRAS thing is currently not supported.
You have to delete the thing, start a scan and add it again.
### Debugging and Tracing
If you want to see what's going on in the binding, switch the log level to DEBUG in the Karaf console
```shell
log:set DEBUG org.openhab.binding.homematic
```
If you want to see even more, switch to TRACE to also see the gateway request/response data
```shell
log:set TRACE org.openhab.binding.homematic
```
Set the logging back to normal
```shell
log:set INFO org.openhab.binding.homematic
```
To identify problems, a full startup TRACE log will be needed:
```shell
stop org.openhab.binding.homematic
log:set TRACE org.openhab.binding.homematic
start org.openhab.binding.homematic
```

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.homematic</artifactId>
<name>openHAB Add-ons :: Bundles :: Homematic Binding</name>
</project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.homematic-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-homematic" description="Homematic Binding" version="${project.version}">
<feature>openhab-transport-upnp</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.homematic/${project.version}</bundle>
</feature>
</features>

View File

@@ -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.homematic.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* Defines common constants, which are used across the binding.
*
* @author Gerhard Riegler - Initial contribution
*/
@NonNullByDefault
public class HomematicBindingConstants {
public static final String BINDING_ID = "homematic";
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
public static final String CONFIG_DESCRIPTION_URI_CHANNEL = "channel-type:homematic:config";
/**
* A thing's config-description-uri is generally composed as follows:<br>
* {@link #CONFIG_DESCRIPTION_URI_THING_PREFIX}:{@link ThingTypeUID}
*/
public static final String CONFIG_DESCRIPTION_URI_THING_PREFIX = "thing-type";
public static final String PROPERTY_VENDOR_NAME = "eQ-3 AG";
public static final String ITEM_TYPE_SWITCH = "Switch";
public static final String ITEM_TYPE_ROLLERSHUTTER = "Rollershutter";
public static final String ITEM_TYPE_CONTACT = "Contact";
public static final String ITEM_TYPE_STRING = "String";
public static final String ITEM_TYPE_NUMBER = "Number";
public static final String ITEM_TYPE_DIMMER = "Dimmer";
public static final String ITEM_TYPE_DATETIME = "DateTime";
public static final String CHANNEL_TYPE_DUTY_CYCLE_RATIO = "DUTY_CYCLE_RATIO";
public static final String CATEGORY_BATTERY = "Battery";
public static final String CATEGORY_ALARM = "Alarm";
public static final String CATEGORY_HUMIDITY = "Humidity";
public static final String CATEGORY_TEMPERATURE = "Temperature";
public static final String CATEGORY_MOTION = "Motion";
public static final String CATEGORY_PRESSURE = "Pressure";
public static final String CATEGORY_SMOKE = "Smoke";
public static final String CATEGORY_WATER = "Water";
public static final String CATEGORY_WIND = "Wind";
public static final String CATEGORY_RAIN = "Rain";
public static final String CATEGORY_ENERGY = "Energy";
public static final String CATEGORY_BLINDS = "Blinds";
public static final String CATEGORY_CONTACT = "Contact";
public static final String CATEGORY_DIMMABLE_LIGHT = "DimmableLight";
public static final String CATEGORY_SWITCH = "Switch";
public static final String PROPERTY_BATTERY_TYPE = "batteryType";
public static final String PROPERTY_AES_KEY = "aesKey";
public static final String PROPERTY_DYNAMIC_FUNCTION_FORMAT = "dynamicFunction-%d";
public static final int INSTALL_MODE_NORMAL = 1;
public static final int CONFIGURATION_CHANNEL_NUMBER = -1;
public static final String RX_BURST_MODE = "BURST";
public static final String RX_WAKEUP_MODE = "WAKEUP";
}

View File

@@ -0,0 +1,410 @@
/**
* 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.homematic.internal.common;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmGatewayInfo;
import org.openhab.binding.homematic.internal.model.HmInterface;
/**
* The main gateway config class.
*
* @author Gerhard Riegler - Initial contribution
*/
public class HomematicConfig {
private static final String ISO_ENCODING = "ISO-8859-1";
private static final String UTF_ENCODING = "UTF-8";
private static final String GATEWAY_TYPE_AUTO = "AUTO";
private static final String GATEWAY_TYPE_CCU = "CCU";
private static final String GATEWAY_TYPE_NOCCU = "NOCCU";
private static final int DEFAULT_PORT_RF = 2001;
private static final int DEFAULT_PORT_WIRED = 2000;
private static final int DEFAULT_PORT_HMIP = 2010;
private static final int DEFAULT_PORT_CUXD = 8701;
private static final int DEFAULT_PORT_GROUP = 9292;
public static final int DEFAULT_INSTALL_MODE_DURATION = 60;
private String gatewayAddress;
private String gatewayType = GATEWAY_TYPE_AUTO;
private int rfPort;
private int wiredPort;
private int hmIpPort;
private int cuxdPort;
private int groupPort;
private String callbackHost;
private String bindAddress;
private int xmlCallbackPort;
private int binCallbackPort;
private int socketMaxAlive = 900;
private int timeout = 15;
private int installModeDuration = DEFAULT_INSTALL_MODE_DURATION;
private long discoveryTimeToLive = -1;
private boolean unpairOnDeletion = false;
private boolean factoryResetOnDeletion = false;
private HmGatewayInfo gatewayInfo;
/**
* Returns the Homematic gateway address.
*/
public String getGatewayAddress() {
return gatewayAddress;
}
/**
* Sets the Homematic gateway address.
*/
public void setGatewayAddress(String gatewayAddress) {
this.gatewayAddress = gatewayAddress;
}
/**
* Returns the callback host address.
*/
public String getCallbackHost() {
return callbackHost;
}
/**
* Sets the callback host address.
*/
public void setCallbackHost(String callbackHost) {
this.callbackHost = callbackHost;
}
/**
* Returns the bind address.
*/
public String getBindAddress() {
return bindAddress;
}
/**
* Sets the bind address.
*/
public void setBindAddress(String bindAddress) {
this.bindAddress = bindAddress;
}
/**
* Sets the callback host port.
*
* @deprecated use setBinCallbackPort
*/
@Deprecated
public void setCallbackPort(int callbackPort) {
this.binCallbackPort = callbackPort;
}
/**
* Returns the XML-RPC callback host port.
*/
public int getXmlCallbackPort() {
return xmlCallbackPort;
}
/**
* Sets the XML-RPC callback host port.
*/
public void setXmlCallbackPort(int xmlCallbackPort) {
this.xmlCallbackPort = xmlCallbackPort;
}
/**
* Returns the BIN-RPC callback host port.
*/
public int getBinCallbackPort() {
return binCallbackPort;
}
/**
* Sets the BIN-RPC callback host port.
*/
public void setBinCallbackPort(int binCallbackPort) {
this.binCallbackPort = binCallbackPort;
}
/**
* Returns the HmGatewayInfo.
*/
public HmGatewayInfo getGatewayInfo() {
return gatewayInfo;
}
/**
* Sets the HmGatewayInfo.
*/
public void setGatewayInfo(HmGatewayInfo gatewayInfo) {
this.gatewayInfo = gatewayInfo;
}
/**
* Returns the max alive time of a socket connection to a Homematic gateway in seconds.
*/
public int getSocketMaxAlive() {
return socketMaxAlive;
}
/**
* Sets the max alive time of a socket connection to a Homematic gateway in seconds.
*/
public void setSocketMaxAlive(int socketMaxAlive) {
this.socketMaxAlive = socketMaxAlive;
}
/**
* Returns the timeout for the communication to a Homematic gateway in seconds.
*/
public int getTimeout() {
return timeout;
}
/**
* Sets the timeout for the communication to a Homematic gateway in seconds.
*/
public void setTimeout(int timeout) {
this.timeout = timeout;
}
/**
* Returns the time to live for discovery results of a Homematic gateway in seconds.
*/
public long getDiscoveryTimeToLive() {
return discoveryTimeToLive;
}
/**
* Sets the time to live for discovery results of a Homematic gateway in seconds.
*/
public void setDiscoveryTimeToLive(long discoveryTimeToLive) {
this.discoveryTimeToLive = discoveryTimeToLive;
}
/**
* Returns the HmGatewayType.
*/
public String getGatewayType() {
return gatewayType;
}
/**
* Sets the HmGatewayType.
*/
public void setGatewayType(String gatewayType) {
this.gatewayType = gatewayType;
}
/**
* Returns time in seconds that the controller will be in install mode when
* a device discovery is initiated
*
* @return time in seconds that the controller remains in install mode
*/
public int getInstallModeDuration() {
return installModeDuration;
}
/**
* Sets installModeDuration
*
* @param installModeDuration time in seconds that the controller remains in
* install mode
*/
public void setInstallModeDuration(int installModeDuration) {
this.installModeDuration = installModeDuration;
}
/**
* Returns if devices are unpaired from the gateway when their corresponding things are removed
*
* @return <i>true</i> if devices are unpaired from the gateway when their corresponding things are removed
*/
public boolean isUnpairOnDeletion() {
return unpairOnDeletion;
}
/**
* Sets unpairOnDeletion
*
* @param unpairOnDeletion if set to <i>true</i>, devices are unpaired from the gateway when their corresponding
* things are removed
*/
public void setUnpairOnDeletion(boolean unpairOnDeletion) {
this.unpairOnDeletion = unpairOnDeletion;
}
/**
* Returns if devices are factory reset when their corresponding things are removed
*
* @return <i>true</i> if devices are factory reset when their corresponding things are removed
*/
public boolean isFactoryResetOnDeletion() {
return factoryResetOnDeletion;
}
/**
* Sets factoryResetOnDeletion
*
* @param factoryResetOnDeletion if set to <i>true</i>, devices are factory reset when their corresponding things
* are removed
*/
public void setFactoryResetOnDeletion(boolean factoryResetOnDeletion) {
this.factoryResetOnDeletion = factoryResetOnDeletion;
}
/**
* Returns the TclRegaScript url.
*/
public String getTclRegaUrl() {
return "http://" + gatewayAddress + ":8181/tclrega.exe";
}
/**
* Returns the Homematic gateway port of the channel.
*/
public int getRpcPort(HmChannel channel) {
return getRpcPort(channel.getDevice().getHmInterface());
}
/**
* Returns the Homematic gateway port of the interfaces.
*/
public int getRpcPort(HmInterface hmInterface) {
if (HmInterface.WIRED.equals(hmInterface)) {
return getWiredPort();
} else if (HmInterface.HMIP.equals(hmInterface)) {
return getHmIpPort();
} else if (HmInterface.CUXD.equals(hmInterface)) {
return getCuxdPort();
} else if (HmInterface.GROUP.equals(hmInterface)) {
return getGroupPort();
} else {
return getRfPort();
}
}
/**
* Returns the port of the RF daemon.
*/
private int getRfPort() {
return rfPort == 0 ? DEFAULT_PORT_RF : rfPort;
}
/**
* Returns the port of the wired daemon.
*/
private int getWiredPort() {
return wiredPort == 0 ? DEFAULT_PORT_WIRED : wiredPort;
}
/**
* Returns the port of the HmIp daemon.
*/
private int getHmIpPort() {
return hmIpPort == 0 ? DEFAULT_PORT_HMIP : hmIpPort;
}
/**
* Returns the port of the CUxD daemon.
*/
private int getCuxdPort() {
return cuxdPort == 0 ? DEFAULT_PORT_CUXD : cuxdPort;
}
/**
* Returns the port of the group daemon.
*/
public int getGroupPort() {
return groupPort == 0 ? DEFAULT_PORT_GROUP : groupPort;
}
/**
* Returns true, if a wired port is configured.
*/
public boolean hasWiredPort() {
return wiredPort != 0;
}
/**
* Returns true, if a hmIp port is configured.
*/
public boolean hasHmIpPort() {
return hmIpPort != 0;
}
/**
* Returns true, if a cuxd port is configured.
*/
public boolean hasCuxdPort() {
return cuxdPort != 0;
}
/**
* Returns true, if a group port is configured.
*/
public boolean hasGroupPort() {
return groupPort != 0;
}
/**
* Returns true, if a RF port is configured.
*/
public boolean hasRfPort() {
return rfPort != 0;
}
/**
* Returns the encoding that is suitable on requests to & responds from the Homematic gateway.
*/
public String getEncoding() {
if (gatewayInfo != null && gatewayInfo.isHomegear()) {
return UTF_ENCODING;
} else {
return ISO_ENCODING;
}
}
/**
* Returns true, if the configured gatewayType is CCU.
*/
public boolean isCCUType() {
return gatewayType.equalsIgnoreCase(HomematicConfig.GATEWAY_TYPE_CCU);
}
/**
* Returns true, if the configured gatewayType is NoCCU.
*/
public boolean isNoCCUType() {
return gatewayType.equalsIgnoreCase(HomematicConfig.GATEWAY_TYPE_NOCCU);
}
@Override
public String toString() {
ToStringBuilder tsb = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
tsb.append("gatewayAddress", gatewayAddress).append("callbackHost", callbackHost)
.append("bindAddress", bindAddress).append("xmlCallbackPort", xmlCallbackPort)
.append("binCallbackPort", binCallbackPort).append("gatewayType", gatewayType)
.append("rfPort", getRfPort()).append("wiredPort", getWiredPort()).append("hmIpPort", getHmIpPort())
.append("cuxdPort", getCuxdPort()).append("groupPort", getGroupPort()).append("timeout", timeout)
.append("discoveryTimeToLive", discoveryTimeToLive).append("installModeDuration", installModeDuration)
.append("socketMaxAlive", socketMaxAlive);
return tsb.toString();
}
}

View File

@@ -0,0 +1,974 @@
/**
* 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.homematic.internal.communicator;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.client.BinRpcClient;
import org.openhab.binding.homematic.internal.communicator.client.RpcClient;
import org.openhab.binding.homematic.internal.communicator.client.TransferMode;
import org.openhab.binding.homematic.internal.communicator.client.UnknownParameterSetException;
import org.openhab.binding.homematic.internal.communicator.client.XmlRpcClient;
import org.openhab.binding.homematic.internal.communicator.parser.ListBidcosInterfacesParser;
import org.openhab.binding.homematic.internal.communicator.server.BinRpcServer;
import org.openhab.binding.homematic.internal.communicator.server.RpcEventListener;
import org.openhab.binding.homematic.internal.communicator.server.RpcServer;
import org.openhab.binding.homematic.internal.communicator.server.XmlRpcServer;
import org.openhab.binding.homematic.internal.communicator.virtual.BatteryTypeVirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.ButtonVirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.DeleteDeviceModeVirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.DeleteDeviceVirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.DisplayOptionsVirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.DisplayTextVirtualDatapoint;
import org.openhab.binding.homematic.internal.communicator.virtual.FirmwareVirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.HmwIoModuleVirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.InstallModeDurationVirtualDatapoint;
import org.openhab.binding.homematic.internal.communicator.virtual.InstallModeVirtualDatapoint;
import org.openhab.binding.homematic.internal.communicator.virtual.OnTimeAutomaticVirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.ReloadAllFromGatewayVirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.ReloadFromGatewayVirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.ReloadRssiVirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.RssiVirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.SignalStrengthVirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.StateContactVirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.VirtualDatapointHandler;
import org.openhab.binding.homematic.internal.communicator.virtual.VirtualGateway;
import org.openhab.binding.homematic.internal.misc.DelayedExecuter;
import org.openhab.binding.homematic.internal.misc.DelayedExecuter.DelayedExecuterCallback;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.misc.HomematicConstants;
import org.openhab.binding.homematic.internal.misc.MiscUtils;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmGatewayInfo;
import org.openhab.binding.homematic.internal.model.HmInterface;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.openhab.binding.homematic.internal.model.HmRssiInfo;
import org.openhab.binding.homematic.internal.model.HmValueType;
import org.openhab.core.common.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AbstractHomematicGateway} is the main class for the communication with a Homematic gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public abstract class AbstractHomematicGateway implements RpcEventListener, HomematicGateway, VirtualGateway {
private final Logger logger = LoggerFactory.getLogger(AbstractHomematicGateway.class);
public static final double DEFAULT_DISABLE_DELAY = 2.0;
private static final long CONNECTION_TRACKER_INTERVAL_SECONDS = 15;
private static final String GATEWAY_POOL_NAME = "homematicGateway";
private final Map<TransferMode, RpcClient<?>> rpcClients = new HashMap<>();
private final Map<TransferMode, RpcServer> rpcServers = new HashMap<>();
protected HomematicConfig config;
protected HttpClient httpClient;
private final String id;
private final HomematicGatewayAdapter gatewayAdapter;
private final DelayedExecuter sendDelayedExecutor = new DelayedExecuter();
private final DelayedExecuter receiveDelayedExecutor = new DelayedExecuter();
private final Set<HmDatapointInfo> echoEvents = Collections.synchronizedSet(new HashSet<>());
private ScheduledFuture<?> connectionTrackerFuture;
private ConnectionTrackerThread connectionTrackerThread;
private final Map<String, HmDevice> devices = Collections.synchronizedMap(new HashMap<>());
private final Map<HmInterface, TransferMode> availableInterfaces = new TreeMap<>();
private static List<VirtualDatapointHandler> virtualDatapointHandlers = new ArrayList<>();
private boolean cancelLoadAllMetadata;
private boolean initialized;
private boolean newDeviceEventsEnabled;
private ScheduledFuture<?> enableNewDeviceFuture;
private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(GATEWAY_POOL_NAME);
static {
// loads all virtual datapoints
virtualDatapointHandlers.add(new BatteryTypeVirtualDatapointHandler());
virtualDatapointHandlers.add(new FirmwareVirtualDatapointHandler());
virtualDatapointHandlers.add(new DisplayOptionsVirtualDatapointHandler());
virtualDatapointHandlers.add(new ReloadFromGatewayVirtualDatapointHandler());
virtualDatapointHandlers.add(new ReloadAllFromGatewayVirtualDatapointHandler());
virtualDatapointHandlers.add(new OnTimeAutomaticVirtualDatapointHandler());
virtualDatapointHandlers.add(new InstallModeVirtualDatapoint());
virtualDatapointHandlers.add(new InstallModeDurationVirtualDatapoint());
virtualDatapointHandlers.add(new DeleteDeviceModeVirtualDatapointHandler());
virtualDatapointHandlers.add(new DeleteDeviceVirtualDatapointHandler());
virtualDatapointHandlers.add(new RssiVirtualDatapointHandler());
virtualDatapointHandlers.add(new ReloadRssiVirtualDatapointHandler());
virtualDatapointHandlers.add(new StateContactVirtualDatapointHandler());
virtualDatapointHandlers.add(new SignalStrengthVirtualDatapointHandler());
virtualDatapointHandlers.add(new DisplayTextVirtualDatapoint());
virtualDatapointHandlers.add(new HmwIoModuleVirtualDatapointHandler());
virtualDatapointHandlers.add(new ButtonVirtualDatapointHandler());
}
public AbstractHomematicGateway(String id, HomematicConfig config, HomematicGatewayAdapter gatewayAdapter,
HttpClient httpClient) {
this.id = id;
this.config = config;
this.gatewayAdapter = gatewayAdapter;
this.httpClient = httpClient;
}
@Override
public void initialize() throws IOException {
logger.debug("Initializing gateway with id '{}'", id);
HmGatewayInfo gatewayInfo = config.getGatewayInfo();
if (gatewayInfo.isHomegear()) {
// Homegear
availableInterfaces.put(HmInterface.RF, TransferMode.BIN_RPC);
} else if (gatewayInfo.isCCU()) {
// CCU
if (gatewayInfo.isRfInterface()) {
availableInterfaces.put(HmInterface.RF, TransferMode.XML_RPC);
}
if (gatewayInfo.isWiredInterface()) {
availableInterfaces.put(HmInterface.WIRED, TransferMode.XML_RPC);
}
if (gatewayInfo.isHmipInterface()) {
availableInterfaces.put(HmInterface.HMIP, TransferMode.XML_RPC);
}
if (gatewayInfo.isCuxdInterface()) {
availableInterfaces.put(HmInterface.CUXD, TransferMode.BIN_RPC);
}
if (gatewayInfo.isGroupInterface()) {
availableInterfaces.put(HmInterface.GROUP, TransferMode.XML_RPC);
}
} else {
// other
if (gatewayInfo.isRfInterface()) {
availableInterfaces.put(HmInterface.RF, TransferMode.XML_RPC);
}
if (gatewayInfo.isWiredInterface()) {
availableInterfaces.put(HmInterface.WIRED, TransferMode.XML_RPC);
}
if (gatewayInfo.isHmipInterface()) {
availableInterfaces.put(HmInterface.HMIP, TransferMode.XML_RPC);
}
}
logger.info("{}", config.getGatewayInfo());
StringBuilder sb = new StringBuilder();
for (Entry<HmInterface, TransferMode> entry : availableInterfaces.entrySet()) {
sb.append(entry.getKey()).append(":").append(entry.getValue()).append(", ");
}
if (sb.length() > 2) {
sb.setLength(sb.length() - 2);
}
logger.debug("Used Homematic transfer modes: {}", sb.toString());
startClients();
startServers();
if (!config.getGatewayInfo().isHomegear()) {
// delay the newDevice event handling at startup, reduces some API calls
long delay = config.getGatewayInfo().isCCU1() ? 10 : 3;
enableNewDeviceFuture = scheduler.schedule(() -> {
newDeviceEventsEnabled = true;
}, delay, TimeUnit.MINUTES);
} else {
newDeviceEventsEnabled = true;
}
}
@Override
public void dispose() {
initialized = false;
if (enableNewDeviceFuture != null) {
enableNewDeviceFuture.cancel(true);
}
newDeviceEventsEnabled = false;
stopWatchdogs();
sendDelayedExecutor.stop();
receiveDelayedExecutor.stop();
stopServers();
stopClients();
devices.clear();
echoEvents.clear();
availableInterfaces.clear();
config.setGatewayInfo(null);
}
/**
* Starts the Homematic gateway client.
*/
protected synchronized void startClients() throws IOException {
for (TransferMode mode : availableInterfaces.values()) {
if (!rpcClients.containsKey(mode)) {
rpcClients.put(mode,
mode == TransferMode.XML_RPC ? new XmlRpcClient(config, httpClient) : new BinRpcClient(config));
}
}
}
/**
* Stops the Homematic gateway client.
*/
protected synchronized void stopClients() {
for (RpcClient<?> rpcClient : rpcClients.values()) {
rpcClient.dispose();
}
rpcClients.clear();
}
/**
* Starts the Homematic RPC server.
*/
private synchronized void startServers() throws IOException {
for (TransferMode mode : availableInterfaces.values()) {
if (!rpcServers.containsKey(mode)) {
RpcServer rpcServer = mode == TransferMode.XML_RPC ? new XmlRpcServer(this, config)
: new BinRpcServer(this, config);
rpcServers.put(mode, rpcServer);
rpcServer.start();
}
}
for (HmInterface hmInterface : availableInterfaces.keySet()) {
getRpcClient(hmInterface).init(hmInterface, hmInterface.toString() + "-" + id);
}
}
/**
* Stops the Homematic RPC server.
*/
private synchronized void stopServers() {
for (HmInterface hmInterface : availableInterfaces.keySet()) {
try {
getRpcClient(hmInterface).release(hmInterface);
} catch (IOException ex) {
// recoverable exception, therefore only debug
logger.debug("Unable to release the connection to the gateway with id '{}': {}", id, ex.getMessage(),
ex);
}
}
for (TransferMode mode : rpcServers.keySet()) {
rpcServers.get(mode).shutdown();
}
rpcServers.clear();
}
@Override
public void startWatchdogs() {
logger.debug("Starting connection tracker for gateway with id '{}'", id);
connectionTrackerThread = new ConnectionTrackerThread();
connectionTrackerFuture = scheduler.scheduleWithFixedDelay(connectionTrackerThread, 30,
CONNECTION_TRACKER_INTERVAL_SECONDS, TimeUnit.SECONDS);
}
private void stopWatchdogs() {
if (connectionTrackerFuture != null) {
connectionTrackerFuture.cancel(true);
}
connectionTrackerThread = null;
}
/**
* Returns the default interface to communicate with the Homematic gateway.
*/
protected HmInterface getDefaultInterface() {
return availableInterfaces.containsKey(HmInterface.RF) ? HmInterface.RF : HmInterface.HMIP;
}
@Override
public RpcClient<?> getRpcClient(HmInterface hmInterface) throws IOException {
RpcClient<?> rpcClient = rpcClients.get(availableInterfaces.get(hmInterface));
if (rpcClient == null) {
throw new IOException("RPC client for interface " + hmInterface + " not available");
}
return rpcClient;
}
/**
* Loads all gateway variables into the given device.
*/
protected abstract void loadVariables(HmChannel channel) throws IOException;
/**
* Loads all gateway scripts into the given device.
*/
protected abstract void loadScripts(HmChannel channel) throws IOException;
/**
* Loads all names of the devices.
*/
protected abstract void loadDeviceNames(Collection<HmDevice> devices) throws IOException;
/**
* Sets a variable on the Homematic gateway.
*/
protected abstract void setVariable(HmDatapoint dp, Object value) throws IOException;
/**
* Execute a script on the Homematic gateway.
*/
protected abstract void executeScript(HmDatapoint dp) throws IOException;
@Override
public HmDatapoint getDatapoint(HmDatapointInfo dpInfo) throws HomematicClientException {
HmDevice device = getDevice(dpInfo.getAddress());
HmChannel channel = device.getChannel(dpInfo.getChannel());
if (channel == null) {
throw new HomematicClientException(String.format("Channel %s in device '%s' not found on gateway '%s'",
dpInfo.getChannel(), dpInfo.getAddress(), id));
}
HmDatapoint dp = channel.getDatapoint(dpInfo);
if (dp == null) {
throw new HomematicClientException(String.format("Datapoint '%s' not found on gateway '%s'", dpInfo, id));
}
return dp;
}
@Override
public HmDevice getDevice(String address) throws HomematicClientException {
HmDevice device = devices.get(address);
if (device == null) {
throw new HomematicClientException(
String.format("Device with address '%s' not found on gateway '%s'", address, id));
}
return device;
}
@Override
public void cancelLoadAllDeviceMetadata() {
cancelLoadAllMetadata = true;
}
@Override
public void loadAllDeviceMetadata() throws IOException {
cancelLoadAllMetadata = false;
// load all device descriptions
List<HmDevice> deviceDescriptions = getDeviceDescriptions();
// loading datapoints for all channels
Set<String> loadedDevices = new HashSet<>();
Map<String, Collection<HmDatapoint>> datapointsByChannelIdCache = new HashMap<>();
for (HmDevice device : deviceDescriptions) {
if (!cancelLoadAllMetadata) {
try {
logger.trace("Loading metadata for device '{}' of type '{}'", device.getAddress(),
device.getType());
if (device.isGatewayExtras()) {
loadChannelValues(device.getChannel(HmChannel.CHANNEL_NUMBER_VARIABLE));
loadChannelValues(device.getChannel(HmChannel.CHANNEL_NUMBER_SCRIPT));
} else {
for (HmChannel channel : device.getChannels()) {
logger.trace(" Loading channel {}", channel);
// speed up metadata generation a little bit for equal channels in the gateway devices
if ((DEVICE_TYPE_VIRTUAL.equals(device.getType())
|| DEVICE_TYPE_VIRTUAL_WIRED.equals(device.getType())) && channel.getNumber() > 1) {
HmChannel previousChannel = device.getChannel(channel.getNumber() - 1);
cloneAllDatapointsIntoChannel(channel, previousChannel.getDatapoints());
} else {
String channelId = String.format("%s:%s:%s", channel.getDevice().getType(),
channel.getDevice().getFirmware(), channel.getNumber());
Collection<HmDatapoint> cachedDatapoints = datapointsByChannelIdCache.get(channelId);
if (cachedDatapoints != null) {
// clone all datapoints
cloneAllDatapointsIntoChannel(channel, cachedDatapoints);
} else {
logger.trace(" Loading datapoints into channel {}", channel);
addChannelDatapoints(channel, HmParamsetType.MASTER);
addChannelDatapoints(channel, HmParamsetType.VALUES);
// Make sure to only cache non-reconfigurable channels. For reconfigurable channels,
// the data point set might change depending on the selected mode.
if (!channel.isReconfigurable()) {
datapointsByChannelIdCache.put(channelId, channel.getDatapoints());
}
}
}
}
}
prepareDevice(device);
loadedDevices.add(device.getAddress());
gatewayAdapter.onDeviceLoaded(device);
} catch (IOException ex) {
logger.warn("Can't load device with address '{}' from gateway '{}': {}", device.getAddress(), id,
ex.getMessage());
}
}
}
if (!cancelLoadAllMetadata) {
devices.keySet().retainAll(loadedDevices);
}
initialized = true;
}
/**
* Loads all datapoints from the gateway.
*/
protected void addChannelDatapoints(HmChannel channel, HmParamsetType paramsetType) throws IOException {
try {
getRpcClient(channel.getDevice().getHmInterface()).addChannelDatapoints(channel, paramsetType);
} catch (UnknownParameterSetException ex) {
logger.info(
"Can not load metadata for device: {}, channel: {}, paramset: {}, maybe there are no channels available",
channel.getDevice().getAddress(), channel.getNumber(), paramsetType);
}
}
/**
* Loads all device descriptions from the gateway.
*/
private List<HmDevice> getDeviceDescriptions() throws IOException {
List<HmDevice> deviceDescriptions = new ArrayList<>();
for (HmInterface hmInterface : availableInterfaces.keySet()) {
deviceDescriptions.addAll(getRpcClient(hmInterface).listDevices(hmInterface));
}
if (!cancelLoadAllMetadata) {
deviceDescriptions.add(createGatewayDevice());
loadDeviceNames(deviceDescriptions);
}
return deviceDescriptions;
}
/**
* Clones all datapoints into the given channel.
*/
private void cloneAllDatapointsIntoChannel(HmChannel channel, Collection<HmDatapoint> datapoints) {
logger.trace(" Cloning {} datapoints into channel {}", datapoints.size(), channel);
for (HmDatapoint dp : datapoints) {
if (!dp.isVirtual()) {
HmDatapoint clonedDp = dp.clone();
clonedDp.setValue(null);
channel.addDatapoint(clonedDp);
}
}
}
@Override
public void loadChannelValues(HmChannel channel) throws IOException {
if (channel.getDevice().isGatewayExtras()) {
if (channel.getNumber() != HmChannel.CHANNEL_NUMBER_EXTRAS) {
List<HmDatapoint> datapoints = channel.getDatapoints();
if (channel.getNumber() == HmChannel.CHANNEL_NUMBER_VARIABLE) {
loadVariables(channel);
logger.debug("Loaded {} gateway variable(s)", datapoints.size());
} else if (channel.getNumber() == HmChannel.CHANNEL_NUMBER_SCRIPT) {
loadScripts(channel);
logger.debug("Loaded {} gateway script(s)", datapoints.size());
}
}
} else {
logger.debug("Loading values for channel {} of device '{}'", channel, channel.getDevice().getAddress());
setChannelDatapointValues(channel, HmParamsetType.MASTER);
setChannelDatapointValues(channel, HmParamsetType.VALUES);
}
for (HmDatapoint dp : channel.getDatapoints()) {
handleVirtualDatapointEvent(dp, false);
}
channel.setInitialized(true);
}
@Override
public void updateChannelValueDatapoints(HmChannel channel) throws IOException {
logger.debug("Updating value datapoints for channel {} of device '{}', has {} datapoints before", channel,
channel.getDevice().getAddress(), channel.getDatapoints().size());
channel.removeValueDatapoints();
addChannelDatapoints(channel, HmParamsetType.VALUES);
setChannelDatapointValues(channel, HmParamsetType.VALUES);
logger.debug("Updated value datapoints for channel {} of device '{}' (function {}), now has {} datapoints",
channel, channel.getDevice().getAddress(), channel.getCurrentFunction(),
channel.getDatapoints().size());
}
/**
* Sets all datapoint values for the given channel.
*/
protected void setChannelDatapointValues(HmChannel channel, HmParamsetType paramsetType) throws IOException {
try {
getRpcClient(channel.getDevice().getHmInterface()).setChannelDatapointValues(channel, paramsetType);
} catch (UnknownParameterSetException ex) {
logger.info(
"Can not load values for device: {}, channel: {}, paramset: {}, maybe there are no values available",
channel.getDevice().getAddress(), channel.getNumber(), paramsetType);
}
}
@Override
public void loadDatapointValue(HmDatapoint dp) throws IOException {
getRpcClient(dp.getChannel().getDevice().getHmInterface()).getDatapointValue(dp);
}
@Override
public void loadRssiValues() throws IOException {
for (HmInterface hmInterface : availableInterfaces.keySet()) {
if (hmInterface == HmInterface.RF) {
List<HmRssiInfo> rssiInfos = getRpcClient(hmInterface).loadRssiInfo(hmInterface);
for (HmRssiInfo hmRssiInfo : rssiInfos) {
updateRssiInfo(hmRssiInfo.getAddress(), DATAPOINT_NAME_RSSI_DEVICE, hmRssiInfo.getDevice());
updateRssiInfo(hmRssiInfo.getAddress(), DATAPOINT_NAME_RSSI_PEER, hmRssiInfo.getPeer());
}
}
}
}
@Override
public void setInstallMode(boolean enable, int seconds) throws IOException {
HmDevice gwExtrasHm = devices.get(HmDevice.ADDRESS_GATEWAY_EXTRAS);
if (gwExtrasHm != null) {
// since the homematic virtual device exist: try setting install mode via its dataPoints
HmDatapoint installModeDataPoint = null;
HmDatapoint installModeDurationDataPoint = null;
// collect virtual datapoints to be accessed
HmChannel hmChannel = gwExtrasHm.getChannel(HmChannel.CHANNEL_NUMBER_EXTRAS);
HmDatapointInfo installModeDurationDataPointInfo = new HmDatapointInfo(HmParamsetType.VALUES, hmChannel,
HomematicConstants.VIRTUAL_DATAPOINT_NAME_INSTALL_MODE_DURATION);
if (enable) {
installModeDurationDataPoint = hmChannel.getDatapoint(installModeDurationDataPointInfo);
}
HmDatapointInfo installModeDataPointInfo = new HmDatapointInfo(HmParamsetType.VALUES, hmChannel,
HomematicConstants.VIRTUAL_DATAPOINT_NAME_INSTALL_MODE);
installModeDataPoint = hmChannel.getDatapoint(installModeDataPointInfo);
// first set duration on the datapoint
if (installModeDurationDataPoint != null) {
try {
VirtualDatapointHandler handler = getVirtualDatapointHandler(installModeDurationDataPoint, null);
handler.handleCommand(this, installModeDurationDataPoint, new HmDatapointConfig(), seconds);
// notify thing if exists
gatewayAdapter.onStateUpdated(installModeDurationDataPoint);
} catch (HomematicClientException ex) {
logger.warn("Failed to send datapoint {}", installModeDurationDataPoint, ex);
}
}
// now that the duration is set, we can enable / disable
if (installModeDataPoint != null) {
try {
VirtualDatapointHandler handler = getVirtualDatapointHandler(installModeDataPoint, null);
handler.handleCommand(this, installModeDataPoint, new HmDatapointConfig(), enable);
// notify thing if exists
gatewayAdapter.onStateUpdated(installModeDataPoint);
return;
} catch (HomematicClientException ex) {
logger.warn("Failed to send datapoint {}", installModeDataPoint, ex);
}
}
}
// no gwExtrasHm available (or previous approach failed), therefore use rpc client directly
for (HmInterface hmInterface : availableInterfaces.keySet()) {
if (hmInterface == HmInterface.RF || hmInterface == HmInterface.CUXD) {
getRpcClient(hmInterface).setInstallMode(hmInterface, enable, seconds);
}
}
}
@Override
public int getInstallMode() throws IOException {
for (HmInterface hmInterface : availableInterfaces.keySet()) {
if (hmInterface == HmInterface.RF || hmInterface == HmInterface.CUXD) {
return getRpcClient(hmInterface).getInstallMode(hmInterface);
}
}
throw new IllegalStateException("Could not determine install mode because no suitable interface exists");
}
private void updateRssiInfo(String address, String datapointName, Integer value) {
HmDatapointInfo dpInfo = new HmDatapointInfo(address, HmParamsetType.VALUES, 0, datapointName);
HmChannel channel;
try {
channel = getDevice(dpInfo.getAddress()).getChannel(0);
if (channel != null) {
eventReceived(dpInfo, value);
}
} catch (HomematicClientException e) {
// ignore
}
}
@Override
public void triggerDeviceValuesReload(HmDevice device) {
logger.debug("Triggering values reload for device '{}'", device.getAddress());
for (HmChannel channel : device.getChannels()) {
channel.setInitialized(false);
}
gatewayAdapter.reloadDeviceValues(device);
}
@Override
public void sendDatapointIgnoreVirtual(HmDatapoint dp, HmDatapointConfig dpConfig, Object newValue)
throws IOException, HomematicClientException {
sendDatapoint(dp, dpConfig, newValue, null, true);
}
@Override
public void sendDatapoint(HmDatapoint dp, HmDatapointConfig dpConfig, Object newValue, String rxMode)
throws IOException, HomematicClientException {
sendDatapoint(dp, dpConfig, newValue, rxMode, false);
}
/**
* Main method for sending datapoints to the gateway. It handles scripts, variables, virtual datapoints, delayed
* executions and auto disabling.
*/
private void sendDatapoint(final HmDatapoint dp, final HmDatapointConfig dpConfig, final Object newValue,
final String rxMode, final boolean ignoreVirtualDatapoints) throws IOException, HomematicClientException {
final HmDatapointInfo dpInfo = new HmDatapointInfo(dp);
if (dp.isPressDatapoint() || (config.getGatewayInfo().isHomegear() && dp.isVariable())) {
echoEvents.add(dpInfo);
}
if (dp.isReadOnly()) {
logger.warn("Datapoint is readOnly, it is not published to the gateway with id '{}': '{}'", id, dpInfo);
} else if (HmValueType.ACTION == dp.getType() && MiscUtils.isFalseValue(newValue)) {
logger.warn(
"Datapoint of type ACTION cannot be set to false, it is not published to the gateway with id '{}': '{}'",
id, dpInfo);
} else {
final VirtualGateway gateway = this;
sendDelayedExecutor.start(dpInfo, dpConfig.getDelay(), new DelayedExecuterCallback() {
@Override
public void execute() throws IOException, HomematicClientException {
VirtualDatapointHandler virtualDatapointHandler = ignoreVirtualDatapoints ? null
: getVirtualDatapointHandler(dp, newValue);
if (virtualDatapointHandler != null) {
logger.debug("Handling virtual datapoint '{}' on gateway with id '{}'", dp.getName(), id);
virtualDatapointHandler.handleCommand(gateway, dp, dpConfig, newValue);
} else if (dp.isScript()) {
if (MiscUtils.isTrueValue(newValue)) {
logger.debug("Executing script '{}' on gateway with id '{}'", dp.getInfo(), id);
executeScript(dp);
}
} else if (dp.isVariable()) {
logger.debug("Sending variable '{}' with value '{}' to gateway with id '{}'", dp.getInfo(),
newValue, id);
setVariable(dp, newValue);
} else {
logger.debug("Sending datapoint '{}' with value '{}' to gateway with id '{}' using rxMode '{}'",
dpInfo, newValue, id, rxMode == null ? "DEFAULT" : rxMode);
getRpcClient(dp.getChannel().getDevice().getHmInterface()).setDatapointValue(dp, newValue,
rxMode);
}
dp.setValue(newValue);
if (MiscUtils.isTrueValue(newValue)
&& (dp.isPressDatapoint() || dp.isScript() || dp.isActionType())) {
disableDatapoint(dp, DEFAULT_DISABLE_DELAY);
}
}
});
}
}
/**
* Returns a VirtualDatapointHandler for the given datapoint if available.
*/
private VirtualDatapointHandler getVirtualDatapointHandler(HmDatapoint dp, Object value) {
for (VirtualDatapointHandler vdph : virtualDatapointHandlers) {
if (vdph.canHandleCommand(dp, value)) {
return vdph;
}
}
return null;
}
private void handleVirtualDatapointEvent(HmDatapoint dp, boolean publishToGateway) {
for (VirtualDatapointHandler vdph : virtualDatapointHandlers) {
if (vdph.canHandleEvent(dp)) {
vdph.handleEvent(this, dp);
if (publishToGateway) {
gatewayAdapter.onStateUpdated(vdph.getVirtualDatapoint(dp.getChannel()));
}
}
}
}
@Override
public void eventReceived(HmDatapointInfo dpInfo, Object newValue) {
String className = newValue == null ? "Unknown" : newValue.getClass().getSimpleName();
logger.debug("Received new ({}) value '{}' for '{}' from gateway with id '{}'", className, newValue, dpInfo,
id);
if (echoEvents.remove(dpInfo)) {
logger.debug("Echo event detected, ignoring '{}'", dpInfo);
} else {
try {
if (connectionTrackerThread != null && dpInfo.isPong() && id.equals(newValue)) {
connectionTrackerThread.pongReceived();
}
if (initialized) {
final HmDatapoint dp = getDatapoint(dpInfo);
HmDatapointConfig config = gatewayAdapter.getDatapointConfig(dp);
receiveDelayedExecutor.start(dpInfo, config.getReceiveDelay(), () -> {
dp.setValue(newValue);
gatewayAdapter.onStateUpdated(dp);
handleVirtualDatapointEvent(dp, true);
if (dp.isPressDatapoint() && MiscUtils.isTrueValue(dp.getValue())) {
disableDatapoint(dp, DEFAULT_DISABLE_DELAY);
}
});
}
} catch (HomematicClientException | IOException ex) {
// ignore
}
}
}
@Override
public void newDevices(List<String> adresses) {
if (initialized && newDeviceEventsEnabled) {
for (String address : adresses) {
try {
logger.debug("New device '{}' detected on gateway with id '{}'", address, id);
List<HmDevice> deviceDescriptions = getDeviceDescriptions();
for (HmDevice device : deviceDescriptions) {
if (device.getAddress().equals(address)) {
for (HmChannel channel : device.getChannels()) {
addChannelDatapoints(channel, HmParamsetType.MASTER);
addChannelDatapoints(channel, HmParamsetType.VALUES);
}
prepareDevice(device);
gatewayAdapter.onNewDevice(device);
}
}
} catch (Exception ex) {
logger.error("{}", ex.getMessage(), ex);
}
}
}
}
@Override
public void deleteDevices(List<String> addresses) {
if (initialized) {
for (String address : addresses) {
logger.debug("Device '{}' removed from gateway with id '{}'", address, id);
HmDevice device = devices.remove(address);
if (device != null) {
gatewayAdapter.onDeviceDeleted(device);
}
}
}
}
@Override
public String getId() {
return id;
}
@Override
public HomematicGatewayAdapter getGatewayAdapter() {
return gatewayAdapter;
}
/**
* Creates a virtual device for handling variables, scripts and other special gateway functions.
*/
private HmDevice createGatewayDevice() {
String type = String.format("%s-%s", HmDevice.TYPE_GATEWAY_EXTRAS, StringUtils.upperCase(id));
HmDevice device = new HmDevice(HmDevice.ADDRESS_GATEWAY_EXTRAS, getDefaultInterface(), type,
config.getGatewayInfo().getId(), null, null);
device.setName(HmDevice.TYPE_GATEWAY_EXTRAS);
device.addChannel(new HmChannel(HmChannel.TYPE_GATEWAY_EXTRAS, HmChannel.CHANNEL_NUMBER_EXTRAS));
device.addChannel(new HmChannel(HmChannel.TYPE_GATEWAY_VARIABLE, HmChannel.CHANNEL_NUMBER_VARIABLE));
device.addChannel(new HmChannel(HmChannel.TYPE_GATEWAY_SCRIPT, HmChannel.CHANNEL_NUMBER_SCRIPT));
return device;
}
/**
* Adds virtual datapoints to the device.
*/
private void prepareDevice(HmDevice device) {
for (VirtualDatapointHandler vdph : virtualDatapointHandlers) {
vdph.initialize(device);
}
devices.put(device.getAddress(), device);
logger.debug("Loaded device '{}' ({}) with {} datapoints", device.getAddress(), device.getType(),
device.getDatapointCount());
if (logger.isTraceEnabled()) {
logger.trace("{}", device);
for (HmChannel channel : device.getChannels()) {
logger.trace(" {}", channel);
for (HmDatapoint dp : channel.getDatapoints()) {
logger.trace(" {}", dp);
}
}
}
}
@Override
public void disableDatapoint(final HmDatapoint dp, double delay) {
try {
sendDelayedExecutor.start(new HmDatapointInfo(dp), delay, new DelayedExecuterCallback() {
@Override
public void execute() throws IOException {
if (MiscUtils.isTrueValue(dp.getValue())) {
dp.setValue(Boolean.FALSE);
gatewayAdapter.onStateUpdated(dp);
handleVirtualDatapointEvent(dp, true);
} else if (dp.getType() == HmValueType.ENUM && dp.getValue() != null && !dp.getValue().equals(0)) {
dp.setValue(dp.getMinValue());
gatewayAdapter.onStateUpdated(dp);
handleVirtualDatapointEvent(dp, true);
}
}
});
} catch (IOException | HomematicClientException ex) {
logger.error("{}", ex.getMessage(), ex);
}
}
@Override
public void deleteDevice(String address, boolean reset, boolean force, boolean defer) {
for (RpcClient<?> rpcClient : rpcClients.values()) {
try {
rpcClient.deleteDevice(getDevice(address), translateFlags(reset, force, defer));
} catch (HomematicClientException e) {
// thrown by getDevice(address) if no device for the given address is paired on the gateway
logger.info("Device deletion not possible: {}", e.getMessage());
} catch (IOException e) {
logger.warn("Device deletion failed: {}", e.getMessage(), e);
}
}
}
private int translateFlags(boolean reset, boolean force, boolean defer) {
final int resetFlag = 0b001;
final int forceFlag = 0b010;
final int deferFlag = 0b100;
int resultFlag = 0;
if (reset) {
resultFlag += resetFlag;
}
if (force) {
resultFlag += forceFlag;
}
if (defer) {
resultFlag += deferFlag;
}
return resultFlag;
}
/**
* Thread which validates the connection to the gateway and restarts the RPC client if necessary.
* It also polls for the current duty cycle ratio of the gateway after every successful connection validation.
*/
private class ConnectionTrackerThread implements Runnable {
private boolean connectionLost;
private boolean ping;
private boolean pong;
@Override
public void run() {
try {
if (ping && !pong) {
handleInvalidConnection("No Pong received!");
}
pong = false;
if (config.getGatewayInfo().isCCU1()) {
// the CCU1 does not support the ping command, we need a workaround
getRpcClient(getDefaultInterface()).listBidcosInterfaces(getDefaultInterface());
// if there is no exception, connection is valid
pongReceived();
} else {
getRpcClient(getDefaultInterface()).ping(getDefaultInterface(), id);
}
ping = true;
try {
updateDutyCycleRatio();
} catch (IOException e) {
logger.debug("Could not read the duty cycle ratio: {}", e.getMessage());
}
} catch (IOException ex) {
try {
handleInvalidConnection("IOException " + ex.getMessage());
} catch (IOException ex2) {
// ignore
}
}
}
public void pongReceived() {
pong = true;
connectionConfirmed();
}
private void updateDutyCycleRatio() throws IOException {
ListBidcosInterfacesParser parser = getRpcClient(getDefaultInterface())
.listBidcosInterfaces(getDefaultInterface());
Integer dutyCycleRatio = parser.getDutyCycleRatio();
if (dutyCycleRatio != null) {
gatewayAdapter.onDutyCycleRatioUpdate(dutyCycleRatio);
}
}
private void connectionConfirmed() {
if (connectionLost) {
connectionLost = false;
logger.info("Connection resumed on gateway '{}'", id);
gatewayAdapter.onConnectionResumed();
}
}
private void handleInvalidConnection(String cause) throws IOException {
ping = false;
if (!connectionLost) {
connectionLost = true;
logger.warn("Connection lost on gateway '{}', cause: \"{}\"", id, cause);
gatewayAdapter.onConnectionLost();
}
stopServers();
stopClients();
startClients();
startServers();
}
}
}

View File

@@ -0,0 +1,240 @@
/**
* 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.homematic.internal.communicator;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.client.UnknownParameterSetException;
import org.openhab.binding.homematic.internal.communicator.client.UnknownRpcFailureException;
import org.openhab.binding.homematic.internal.communicator.parser.CcuLoadDeviceNamesParser;
import org.openhab.binding.homematic.internal.communicator.parser.CcuParamsetDescriptionParser;
import org.openhab.binding.homematic.internal.communicator.parser.CcuValueParser;
import org.openhab.binding.homematic.internal.communicator.parser.CcuVariablesAndScriptsParser;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.openhab.binding.homematic.internal.model.HmResult;
import org.openhab.binding.homematic.internal.model.TclScript;
import org.openhab.binding.homematic.internal.model.TclScriptDataList;
import org.openhab.binding.homematic.internal.model.TclScriptList;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.StaxDriver;
/**
* HomematicGateway implementation for a CCU.
*
* @author Gerhard Riegler - Initial contribution
*/
public class CcuGateway extends AbstractHomematicGateway {
private final Logger logger = LoggerFactory.getLogger(CcuGateway.class);
private Map<String, String> tclregaScripts;
private XStream xStream = new XStream(new StaxDriver());
protected CcuGateway(String id, HomematicConfig config, HomematicGatewayAdapter gatewayAdapter,
HttpClient httpClient) {
super(id, config, gatewayAdapter, httpClient);
xStream.setClassLoader(CcuGateway.class.getClassLoader());
xStream.autodetectAnnotations(true);
xStream.alias("scripts", TclScriptList.class);
xStream.alias("list", TclScriptDataList.class);
xStream.alias("result", HmResult.class);
}
@Override
protected void startClients() throws IOException {
super.startClients();
tclregaScripts = loadTclRegaScripts();
}
@Override
protected void stopClients() {
super.stopClients();
tclregaScripts = null;
}
@Override
protected void loadVariables(HmChannel channel) throws IOException {
TclScriptDataList resultList = sendScriptByName("getAllVariables", TclScriptDataList.class);
new CcuVariablesAndScriptsParser(channel).parse(resultList);
}
@Override
protected void loadScripts(HmChannel channel) throws IOException {
TclScriptDataList resultList = sendScriptByName("getAllPrograms", TclScriptDataList.class);
new CcuVariablesAndScriptsParser(channel).parse(resultList);
}
@Override
protected void loadDeviceNames(Collection<HmDevice> devices) throws IOException {
TclScriptDataList resultList = sendScriptByName("getAllDeviceNames", TclScriptDataList.class);
new CcuLoadDeviceNamesParser(devices).parse(resultList);
}
@Override
protected void setChannelDatapointValues(HmChannel channel, HmParamsetType paramsetType) throws IOException {
try {
super.setChannelDatapointValues(channel, paramsetType);
} catch (UnknownRpcFailureException ex) {
logger.debug(
"RpcMessage unknown RPC failure (-1 Failure), fetching values with TclRega script for device {}, channel: {}, paramset: {}",
channel.getDevice().getAddress(), channel.getNumber(), paramsetType);
Collection<String> dpNames = new ArrayList<>();
for (HmDatapoint dp : channel.getDatapoints()) {
if (!dp.isVirtual() && dp.isReadable() && dp.getParamsetType() == HmParamsetType.VALUES) {
dpNames.add(dp.getName());
}
}
if (!dpNames.isEmpty()) {
HmDevice device = channel.getDevice();
String channelName = String.format("%s.%s:%s.", device.getHmInterface().getName(), device.getAddress(),
channel.getNumber());
String datapointNames = StringUtils.join(dpNames.toArray(), "\\t");
TclScriptDataList resultList = sendScriptByName("getAllChannelValues", TclScriptDataList.class,
new String[] { "channel_name", "datapoint_names" },
new String[] { channelName, datapointNames });
new CcuValueParser(channel).parse(resultList);
channel.setInitialized(true);
}
}
}
@Override
protected void addChannelDatapoints(HmChannel channel, HmParamsetType paramsetType) throws IOException {
try {
getRpcClient(channel.getDevice().getHmInterface()).addChannelDatapoints(channel, paramsetType);
} catch (UnknownParameterSetException ex) {
logger.debug(
"RpcMessage RPC failure (-3 Unknown paramset), fetching metadata with TclRega script for device: {}, channel: {}, paramset: {}",
channel.getDevice().getAddress(), channel.getNumber(), paramsetType);
TclScriptDataList resultList = sendScriptByName("getParamsetDescription", TclScriptDataList.class,
new String[] { "device_address", "channel_number" },
new String[] { channel.getDevice().getAddress(), channel.getNumber().toString() });
new CcuParamsetDescriptionParser(channel, paramsetType).parse(resultList);
}
}
@Override
protected void setVariable(HmDatapoint dp, Object value) throws IOException {
String strValue = StringUtils.replace(ObjectUtils.toString(value), "\"", "\\\"");
if (dp.isStringType()) {
strValue = "\"" + strValue + "\"";
}
HmResult result = sendScriptByName("setVariable", HmResult.class,
new String[] { "variable_name", "variable_state" }, new String[] { dp.getInfo(), strValue });
if (!result.isValid()) {
throw new IOException("Unable to set CCU variable " + dp.getInfo());
}
}
@Override
protected void executeScript(HmDatapoint dp) throws IOException {
HmResult result = sendScriptByName("executeProgram", HmResult.class, new String[] { "program_name" },
new String[] { dp.getInfo() });
if (!result.isValid()) {
throw new IOException("Unable to start CCU program: " + dp.getInfo());
}
}
/**
* Sends a TclRega script to the CCU.
*/
private <T> T sendScriptByName(String scriptName, Class<T> clazz) throws IOException {
return sendScriptByName(scriptName, clazz, new String[] {}, null);
}
/**
* Sends a TclRega script with the specified variables to the CCU.
*/
private <T> T sendScriptByName(String scriptName, Class<T> clazz, String[] variableNames, String[] values)
throws IOException {
String script = tclregaScripts.get(scriptName);
for (int i = 0; i < variableNames.length; i++) {
script = StringUtils.replace(script, "{" + variableNames[i] + "}", values[i]);
}
return sendScript(script, clazz);
}
/**
* Main method for sending a TclRega script and parsing the XML result.
*/
@SuppressWarnings("unchecked")
private synchronized <T> T sendScript(String script, Class<T> clazz) throws IOException {
try {
script = StringUtils.trim(script);
if (StringUtils.isEmpty(script)) {
throw new RuntimeException("Homematic TclRegaScript is empty!");
}
if (logger.isTraceEnabled()) {
logger.trace("TclRegaScript: {}", script);
}
StringContentProvider content = new StringContentProvider(script, config.getEncoding());
ContentResponse response = httpClient.POST(config.getTclRegaUrl()).content(content)
.timeout(config.getTimeout(), TimeUnit.SECONDS)
.header(HttpHeader.CONTENT_TYPE, "text/plain;charset=" + config.getEncoding()).send();
String result = new String(response.getContent(), config.getEncoding());
result = StringUtils.substringBeforeLast(result, "<xml><exec>");
if (logger.isTraceEnabled()) {
logger.trace("Result TclRegaScript: {}", result);
}
return (T) xStream.fromXML(result);
} catch (Exception ex) {
throw new IOException(ex.getMessage(), ex);
}
}
/**
* Load predefined scripts from an XML file.
*/
private Map<String, String> loadTclRegaScripts() throws IOException {
Bundle bundle = FrameworkUtil.getBundle(getClass());
try (InputStream stream = bundle.getResource("homematic/tclrega-scripts.xml").openStream()) {
TclScriptList scriptList = (TclScriptList) xStream.fromXML(stream);
Map<String, String> result = new HashMap<>();
if (scriptList.getScripts() != null) {
for (TclScript script : scriptList.getScripts()) {
result.put(script.name, StringUtils.trimToNull(script.data));
}
}
return result;
} catch (IllegalStateException | IOException e) {
throw new IOException("The resource homematic/tclrega-scripts.xml could not be loaded!", e);
}
}
}

View File

@@ -0,0 +1,60 @@
/**
* 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.homematic.internal.communicator;
import java.io.IOException;
import java.util.Collection;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDevice;
/**
* Default HomematicGateway implementation for RF and HS485 daemons.
*
* @author Gerhard Riegler - Initial contribution
*/
public class DefaultGateway extends AbstractHomematicGateway {
protected DefaultGateway(String id, HomematicConfig config, HomematicGatewayAdapter gatewayAdapter,
HttpClient httpClient) {
super(id, config, gatewayAdapter, httpClient);
}
@Override
protected void loadVariables(HmChannel channel) throws IOException {
// not supported
}
@Override
protected void loadScripts(HmChannel channel) throws IOException {
// not supported
}
@Override
protected void setVariable(HmDatapoint dp, Object value) throws IOException {
// not supported
}
@Override
protected void executeScript(HmDatapoint dp) throws IOException {
// not supported
}
@Override
protected void loadDeviceNames(Collection<HmDevice> devices) throws IOException {
// not supported
}
}

View File

@@ -0,0 +1,60 @@
/**
* 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.homematic.internal.communicator;
import java.io.IOException;
import java.util.Collection;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDevice;
/**
* HomematicGateway implementation for Homegear.
*
* @author Gerhard Riegler - Initial contribution
*/
public class HomegearGateway extends AbstractHomematicGateway {
protected HomegearGateway(String id, HomematicConfig config, HomematicGatewayAdapter gatewayAdapter,
HttpClient httpClient) {
super(id, config, gatewayAdapter, httpClient);
}
@Override
protected void loadVariables(HmChannel channel) throws IOException {
getRpcClient(getDefaultInterface()).getAllSystemVariables(channel);
}
@Override
protected void loadScripts(HmChannel channel) throws IOException {
getRpcClient(getDefaultInterface()).getAllScripts(channel);
}
@Override
protected void setVariable(HmDatapoint dp, Object value) throws IOException {
getRpcClient(getDefaultInterface()).setSystemVariable(dp, value);
}
@Override
protected void executeScript(HmDatapoint dp) throws IOException {
getRpcClient(getDefaultInterface()).executeScript(dp);
}
@Override
protected void loadDeviceNames(Collection<HmDevice> devices) throws IOException {
getRpcClient(getDefaultInterface()).loadDeviceNames(getDefaultInterface(), devices);
}
}

View File

@@ -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.homematic.internal.communicator;
import java.io.IOException;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.binding.homematic.internal.model.HmDevice;
/**
* Describes the methods required for the communication with a Homematic gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public interface HomematicGateway {
/**
* Initializes the Homematic gateway and starts the watchdog thread.
*/
public void initialize() throws IOException;
/**
* Disposes the HomematicGateway and stops everything.
*/
public void dispose();
/**
* Returns the cached datapoint.
*/
public HmDatapoint getDatapoint(HmDatapointInfo dpInfo) throws HomematicClientException;
/**
* Returns the cached device.
*/
public HmDevice getDevice(String address) throws HomematicClientException;
/**
* Cancel loading all device metadata.
*/
public void cancelLoadAllDeviceMetadata();
/**
* Loads all device, channel and datapoint metadata from the gateway.
*/
public void loadAllDeviceMetadata() throws IOException;
/**
* Loads all values into the given channel.
*/
public void loadChannelValues(HmChannel channel) throws IOException;
/**
* Loads the value of the given {@link HmDatapoint} from the device.
*
* @param dp The HmDatapoint that shall be loaded
*/
public void loadDatapointValue(HmDatapoint dp) throws IOException;
/**
* Reenumerates the set of VALUES datapoints for the given channel.
*/
public void updateChannelValueDatapoints(HmChannel channel) throws IOException;
/**
* Prepares the device for reloading all values from the gateway.
*/
public void triggerDeviceValuesReload(HmDevice device);
/**
* Sends the datapoint to the Homematic gateway or executes virtual datapoints.
*
* @param dp The datapoint to send/execute
* @param dpConfig The configuration of the datapoint
* @param newValue The new value for the datapoint
* @param rxMode The rxMode with which the value should be sent to the device
* ({@link HomematicBindingConstants#RX_BURST_MODE "BURST"} for burst mode,
* {@link HomematicBindingConstants#RX_WAKEUP_MODE "WAKEUP"} for wakeup mode, or null for the default
* mode)
*/
public void sendDatapoint(HmDatapoint dp, HmDatapointConfig dpConfig, Object newValue, String rxMode)
throws IOException, HomematicClientException;
/**
* Returns the id of the HomematicGateway.
*/
public String getId();
/**
* Set install mode of homematic controller. During install mode the
* controller will accept any device (normal mode)
*
* @param enable <i>true</i> will start install mode, whereas <i>false</i>
* will stop it
* @param seconds specify how long the install mode should last
* @throws IOException if RpcClient fails to propagate command
*/
public void setInstallMode(boolean enable, int seconds) throws IOException;
/**
* Get current install mode of homematic contoller
*
* @return the current time in seconds that the controller remains in
* <i>install_mode==true</i>, respectively <i>0</i> in case of
* <i>install_mode==false</i>
* @throws IOException if RpcClient fails to propagate command
*/
public int getInstallMode() throws IOException;
/**
* Loads all rssi values from the gateway.
*/
public void loadRssiValues() throws IOException;
/**
* Starts the connection and event tracker threads.
*/
public void startWatchdogs();
/**
* Deletes the device from the gateway.
*
* @param address The address of the device to be deleted
* @param reset <i>true</i> will perform a factory reset on the device before deleting it.
* @param force <i>true</i> will delete the device even if it is not reachable.
* @param defer <i>true</i> will delete the device once it becomes available.
*/
public void deleteDevice(String address, boolean reset, boolean force, boolean defer);
}

View File

@@ -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.homematic.internal.communicator;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDevice;
/**
* Adapter with methods called from events within the {@link HomematicGateway} class.
*
* @author Gerhard Riegler - Initial contribution
*/
public interface HomematicGatewayAdapter {
/**
* Called when a datapoint has been updated.
*/
public void onStateUpdated(HmDatapoint dp);
/**
* Called when a new device has been detected on the gateway.
*/
public void onNewDevice(HmDevice device);
/**
* Called when a device has been deleted from the gateway.
*/
public void onDeviceDeleted(HmDevice device);
/**
* Called when the devices values should be reloaded from the gateway.
*/
public void reloadDeviceValues(HmDevice device);
/**
* Called when all values for all devices should be reloaded from the gateway.
*/
public void reloadAllDeviceValues();
/**
* Called when a device has been loaded from the gateway.
*/
public void onDeviceLoaded(HmDevice device);
/**
* Called when the connection is lost to the gateway.
*/
public void onConnectionLost();
/**
* Called when the connection is resumed to the gateway.
*/
public void onConnectionResumed();
/**
* Returns the configuration of a datapoint.
*/
public HmDatapointConfig getDatapointConfig(HmDatapoint dp);
/**
* Called when a new value for the duty cycle of the gateway has been received.
*/
public void onDutyCycleRatioUpdate(int dutyCycleRatio);
}

View File

@@ -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.homematic.internal.communicator;
import java.io.IOException;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.client.RpcClient;
import org.openhab.binding.homematic.internal.communicator.client.XmlRpcClient;
/**
* Factory which evaluates the type of the Homematic gateway and instantiates the appropriate class.
*
* @author Gerhard Riegler - Initial contribution
*/
public class HomematicGatewayFactory {
/**
* Creates the HomematicGateway.
*/
public static HomematicGateway createGateway(String id, HomematicConfig config,
HomematicGatewayAdapter gatewayAdapter, HttpClient httpClient) throws IOException {
loadGatewayInfo(config, id, httpClient);
if (config.getGatewayInfo().isCCU()) {
return new CcuGateway(id, config, gatewayAdapter, httpClient);
} else if (config.getGatewayInfo().isHomegear()) {
return new HomegearGateway(id, config, gatewayAdapter, httpClient);
} else {
return new DefaultGateway(id, config, gatewayAdapter, httpClient);
}
}
/**
* Loads some metadata about the type of the Homematic gateway.
*/
private static void loadGatewayInfo(HomematicConfig config, String id, HttpClient httpClient) throws IOException {
RpcClient<String> rpcClient = new XmlRpcClient(config, httpClient);
try {
config.setGatewayInfo(rpcClient.getGatewayInfo(id));
} finally {
rpcClient.dispose();
}
}
}

View File

@@ -0,0 +1,102 @@
/**
* 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.homematic.internal.communicator.client;
import java.io.IOException;
import java.net.Socket;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.message.BinRpcMessage;
import org.openhab.binding.homematic.internal.communicator.message.RpcRequest;
import org.openhab.binding.homematic.internal.communicator.parser.RpcResponseParser;
import org.openhab.binding.homematic.internal.model.HmInterface;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Client implementation for sending messages via BIN-RPC to the Homematic server.
*
* @author Gerhard Riegler - Initial contribution
*/
public class BinRpcClient extends RpcClient<byte[]> {
private final Logger logger = LoggerFactory.getLogger(BinRpcClient.class);
private SocketHandler socketHandler;
public BinRpcClient(HomematicConfig config) {
super(config);
socketHandler = new SocketHandler(config);
}
@Override
public void dispose() {
socketHandler.flush();
}
@Override
protected RpcRequest<byte[]> createRpcRequest(String methodName) {
return new BinRpcMessage(methodName, config.getEncoding());
}
@Override
protected String getRpcCallbackUrl() {
return "binary://" + config.getCallbackHost() + ":" + config.getBinCallbackPort();
}
@Override
public void init(HmInterface hmInterface, String clientId) throws IOException {
super.init(hmInterface, clientId);
socketHandler.removeSocket(config.getRpcPort(hmInterface));
}
/**
* Sends a BIN-RPC message and parses the response to see if there was an error.
*/
@Override
protected synchronized Object[] sendMessage(int port, RpcRequest<byte[]> request) throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("Client BinRpcRequest:\n{}", request);
}
return sendMessage(port, request, 0);
}
/**
* Sends the message, retries if there was an error.
*/
private Object[] sendMessage(int port, RpcRequest<byte[]> request, int rpcRetryCounter) throws IOException {
BinRpcMessage resp = null;
try {
Socket socket = socketHandler.getSocket(port);
socket.getOutputStream().write(request.createMessage());
resp = new BinRpcMessage(socket.getInputStream(), false, config.getEncoding());
return new RpcResponseParser(request).parse(resp.getResponseData());
} catch (UnknownRpcFailureException | UnknownParameterSetException rpcEx) {
// throw immediately, don't retry the message
throw rpcEx;
} catch (IOException ioEx) {
if ("init".equals(request.getMethodName()) || rpcRetryCounter >= MAX_RPC_RETRY) {
throw ioEx;
} else {
rpcRetryCounter++;
logger.debug("BinRpcMessage socket failure, sending message again {}/{}", rpcRetryCounter,
MAX_RPC_RETRY);
socketHandler.removeSocket(port);
return sendMessage(port, request, rpcRetryCounter);
}
} finally {
if (logger.isTraceEnabled()) {
logger.trace("Client BinRpcResponse:\n{}", resp == null ? "null" : resp.toString());
}
}
}
}

View File

@@ -0,0 +1,466 @@
/**
* 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.homematic.internal.communicator.client;
import static org.openhab.binding.homematic.internal.HomematicBindingConstants.*;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.homematic.internal.HomematicBindingConstants;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.message.RpcRequest;
import org.openhab.binding.homematic.internal.communicator.parser.GetAllScriptsParser;
import org.openhab.binding.homematic.internal.communicator.parser.GetAllSystemVariablesParser;
import org.openhab.binding.homematic.internal.communicator.parser.GetDeviceDescriptionParser;
import org.openhab.binding.homematic.internal.communicator.parser.GetParamsetDescriptionParser;
import org.openhab.binding.homematic.internal.communicator.parser.GetParamsetParser;
import org.openhab.binding.homematic.internal.communicator.parser.GetValueParser;
import org.openhab.binding.homematic.internal.communicator.parser.HomegearLoadDeviceNamesParser;
import org.openhab.binding.homematic.internal.communicator.parser.ListBidcosInterfacesParser;
import org.openhab.binding.homematic.internal.communicator.parser.ListDevicesParser;
import org.openhab.binding.homematic.internal.communicator.parser.RssiInfoParser;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmGatewayInfo;
import org.openhab.binding.homematic.internal.model.HmInterface;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.openhab.binding.homematic.internal.model.HmRssiInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Client implementation for sending messages via BIN-RPC to a Homematic gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public abstract class RpcClient<T> {
private final Logger logger = LoggerFactory.getLogger(RpcClient.class);
protected static final int MAX_RPC_RETRY = 3;
protected static final int RESP_BUFFER_SIZE = 8192;
protected HomematicConfig config;
public RpcClient(HomematicConfig config) {
this.config = config;
}
/**
* Disposes the client.
*/
public abstract void dispose();
/**
* Returns a RpcRequest for this client.
*/
protected abstract RpcRequest<T> createRpcRequest(String methodName);
/**
* Returns the callback url for this client.
*/
protected abstract String getRpcCallbackUrl();
/**
* Sends the RPC message to the gateway.
*/
protected abstract Object[] sendMessage(int port, RpcRequest<T> request) throws IOException;
/**
* Register a callback for the specified interface where the Homematic gateway can send its events.
*/
public void init(HmInterface hmInterface, String clientId) throws IOException {
RpcRequest<T> request = createRpcRequest("init");
request.addArg(getRpcCallbackUrl());
request.addArg(clientId);
if (config.getGatewayInfo().isHomegear()) {
request.addArg(new Integer(0x22));
}
sendMessage(config.getRpcPort(hmInterface), request);
}
/**
* Release a callback for the specified interface.
*/
public void release(HmInterface hmInterface) throws IOException {
RpcRequest<T> request = createRpcRequest("init");
request.addArg(getRpcCallbackUrl());
sendMessage(config.getRpcPort(hmInterface), request);
}
/**
* Sends a ping to the specified interface.
*/
public void ping(HmInterface hmInterface, String callerId) throws IOException {
RpcRequest<T> request = createRpcRequest("ping");
request.addArg(callerId);
sendMessage(config.getRpcPort(hmInterface), request);
}
/**
* Returns the info of all BidCos interfaces available on the gateway.
*/
public ListBidcosInterfacesParser listBidcosInterfaces(HmInterface hmInterface) throws IOException {
RpcRequest<T> request = createRpcRequest("listBidcosInterfaces");
return new ListBidcosInterfacesParser().parse(sendMessage(config.getRpcPort(hmInterface), request));
}
/**
* Returns some infos of the gateway.
*/
private GetDeviceDescriptionParser getDeviceDescription(HmInterface hmInterface) throws IOException {
RpcRequest<T> request = createRpcRequest("getDeviceDescription");
request.addArg("BidCoS-RF");
return new GetDeviceDescriptionParser().parse(sendMessage(config.getRpcPort(hmInterface), request));
}
/**
* Returns all variable metadata and values from a Homegear gateway.
*/
public void getAllSystemVariables(HmChannel channel) throws IOException {
RpcRequest<T> request = createRpcRequest("getAllSystemVariables");
new GetAllSystemVariablesParser(channel).parse(sendMessage(config.getRpcPort(channel), request));
}
/**
* Loads all device names from a Homegear gateway.
*/
public void loadDeviceNames(HmInterface hmInterface, Collection<HmDevice> devices) throws IOException {
RpcRequest<T> request = createRpcRequest("getDeviceInfo");
new HomegearLoadDeviceNamesParser(devices).parse(sendMessage(config.getRpcPort(hmInterface), request));
}
/**
* Returns true, if the interface is available on the gateway.
*/
public void checkInterface(HmInterface hmInterface) throws IOException {
RpcRequest<T> request = createRpcRequest("init");
request.addArg("http://openhab.validation:1000");
sendMessage(config.getRpcPort(hmInterface), request);
}
/**
* Returns all script metadata from a Homegear gateway.
*/
public void getAllScripts(HmChannel channel) throws IOException {
RpcRequest<T> request = createRpcRequest("getAllScripts");
new GetAllScriptsParser(channel).parse(sendMessage(config.getRpcPort(channel), request));
}
/**
* Returns all device and channel metadata.
*/
public Collection<HmDevice> listDevices(HmInterface hmInterface) throws IOException {
RpcRequest<T> request = createRpcRequest("listDevices");
return new ListDevicesParser(hmInterface, config).parse(sendMessage(config.getRpcPort(hmInterface), request));
}
/**
* Loads all datapoint metadata into the given channel.
*/
public void addChannelDatapoints(HmChannel channel, HmParamsetType paramsetType) throws IOException {
if (isConfigurationChannel(channel) && paramsetType != HmParamsetType.MASTER) {
// The configuration channel only has a MASTER Paramset, so there is nothing to load
return;
}
RpcRequest<T> request = createRpcRequest("getParamsetDescription");
request.addArg(getRpcAddress(channel.getDevice().getAddress()) + getChannelSuffix(channel));
request.addArg(paramsetType.toString());
new GetParamsetDescriptionParser(channel, paramsetType).parse(sendMessage(config.getRpcPort(channel), request));
}
/**
* Sets all datapoint values for the given channel.
*/
public void setChannelDatapointValues(HmChannel channel, HmParamsetType paramsetType) throws IOException {
if (isConfigurationChannel(channel) && paramsetType != HmParamsetType.MASTER) {
// The configuration channel only has a MASTER Paramset, so there is nothing to load
return;
}
RpcRequest<T> request = createRpcRequest("getParamset");
request.addArg(getRpcAddress(channel.getDevice().getAddress()) + getChannelSuffix(channel));
request.addArg(paramsetType.toString());
if (channel.getDevice().getHmInterface() == HmInterface.CUXD && paramsetType == HmParamsetType.VALUES) {
setChannelDatapointValues(channel);
} else {
try {
new GetParamsetParser(channel, paramsetType).parse(sendMessage(config.getRpcPort(channel), request));
} catch (UnknownRpcFailureException ex) {
if (paramsetType == HmParamsetType.VALUES) {
logger.debug(
"RpcResponse unknown RPC failure (-1 Failure), fetching values with another API method for device: {}, channel: {}, paramset: {}",
channel.getDevice().getAddress(), channel.getNumber(), paramsetType);
setChannelDatapointValues(channel);
} else {
throw ex;
}
}
}
}
/**
* Reads all VALUES datapoints individually, fallback method if setChannelDatapointValues throws a -1 Failure
* exception.
*/
private void setChannelDatapointValues(HmChannel channel) throws IOException {
for (HmDatapoint dp : channel.getDatapoints()) {
getDatapointValue(dp);
}
}
/**
* Tries to identify the gateway and returns the GatewayInfo.
*/
public HmGatewayInfo getGatewayInfo(String id) throws IOException {
boolean isHomegear = false;
GetDeviceDescriptionParser ddParser;
ListBidcosInterfacesParser biParser;
try {
ddParser = getDeviceDescription(HmInterface.RF);
isHomegear = StringUtils.equalsIgnoreCase(ddParser.getType(), "Homegear");
} catch (IOException ex) {
// can't load gateway infos via RF interface
ddParser = new GetDeviceDescriptionParser();
}
try {
biParser = listBidcosInterfaces(HmInterface.RF);
} catch (IOException ex) {
biParser = listBidcosInterfaces(HmInterface.HMIP);
}
HmGatewayInfo gatewayInfo = new HmGatewayInfo();
gatewayInfo.setAddress(biParser.getGatewayAddress());
if (isHomegear) {
gatewayInfo.setId(HmGatewayInfo.ID_HOMEGEAR);
gatewayInfo.setType(ddParser.getType());
gatewayInfo.setFirmware(ddParser.getFirmware());
} else if ((StringUtils.startsWithIgnoreCase(biParser.getType(), "CCU")
|| StringUtils.startsWithIgnoreCase(biParser.getType(), "HMIP_CCU")
|| StringUtils.startsWithIgnoreCase(ddParser.getType(), "HM-RCV-50") || config.isCCUType())
&& !config.isNoCCUType()) {
gatewayInfo.setId(HmGatewayInfo.ID_CCU);
String type = StringUtils.isBlank(biParser.getType()) ? "CCU" : biParser.getType();
gatewayInfo.setType(type);
gatewayInfo.setFirmware(ddParser.getFirmware() != null ? ddParser.getFirmware() : biParser.getFirmware());
} else {
gatewayInfo.setId(HmGatewayInfo.ID_DEFAULT);
gatewayInfo.setType(biParser.getType());
gatewayInfo.setFirmware(biParser.getFirmware());
}
if (gatewayInfo.isCCU() || config.hasRfPort()) {
gatewayInfo.setRfInterface(hasInterface(HmInterface.RF, id));
}
if (gatewayInfo.isCCU() || config.hasWiredPort()) {
gatewayInfo.setWiredInterface(hasInterface(HmInterface.WIRED, id));
}
if (gatewayInfo.isCCU() || config.hasHmIpPort()) {
gatewayInfo.setHmipInterface(hasInterface(HmInterface.HMIP, id));
}
if (gatewayInfo.isCCU() || config.hasCuxdPort()) {
gatewayInfo.setCuxdInterface(hasInterface(HmInterface.CUXD, id));
}
if (gatewayInfo.isCCU() || config.hasGroupPort()) {
gatewayInfo.setGroupInterface(hasInterface(HmInterface.GROUP, id));
}
return gatewayInfo;
}
/**
* Returns true, if a connection is possible with the given interface.
*/
private boolean hasInterface(HmInterface hmInterface, String id) throws IOException {
try {
checkInterface(hmInterface);
return true;
} catch (IOException ex) {
logger.info("Interface '{}' on gateway '{}' not available, disabling support", hmInterface, id);
return false;
}
}
/**
* Sets the value of the datapoint using the provided rx transmission mode.
*
* @param dp The datapoint to set
* @param value The new value to set on the datapoint
* @param rxMode The rx mode to use for the transmission of the datapoint value
* ({@link HomematicBindingConstants#RX_BURST_MODE "BURST"} for burst mode,
* {@link HomematicBindingConstants#RX_WAKEUP_MODE "WAKEUP"} for wakeup mode, or null for the default
* mode)
*/
public void setDatapointValue(HmDatapoint dp, Object value, String rxMode) throws IOException {
if (dp.isIntegerType() && value instanceof Double) {
value = ((Number) value).intValue();
}
RpcRequest<T> request;
if (HmParamsetType.VALUES == dp.getParamsetType()) {
request = createRpcRequest("setValue");
request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
request.addArg(dp.getName());
request.addArg(value);
configureRxMode(request, rxMode);
} else {
request = createRpcRequest("putParamset");
request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
request.addArg(HmParamsetType.MASTER.toString());
Map<String, Object> paramSet = new HashMap<>();
paramSet.put(dp.getName(), value);
request.addArg(paramSet);
configureRxMode(request, rxMode);
}
sendMessage(config.getRpcPort(dp.getChannel()), request);
}
protected void configureRxMode(RpcRequest<T> request, String rxMode) {
if (rxMode != null) {
if (RX_BURST_MODE.equals(rxMode) || RX_WAKEUP_MODE.equals(rxMode)) {
request.addArg(rxMode);
}
}
}
/**
* Retrieves the value of a single {@link HmDatapoint} from the device. Can only be used for the paramset "VALUES".
*
* @param dp The HmDatapoint that shall be loaded
* @throws IOException If there is a problem while communicating to the gateway
*/
public void getDatapointValue(HmDatapoint dp) throws IOException {
if (dp.isReadable() && !dp.isVirtual() && dp.getParamsetType() == HmParamsetType.VALUES) {
RpcRequest<T> request = createRpcRequest("getValue");
request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
request.addArg(dp.getName());
new GetValueParser(dp).parse(sendMessage(config.getRpcPort(dp.getChannel()), request));
}
}
/**
* Sets the value of a system variable on a Homegear gateway.
*/
public void setSystemVariable(HmDatapoint dp, Object value) throws IOException {
RpcRequest<T> request = createRpcRequest("setSystemVariable");
request.addArg(dp.getInfo());
request.addArg(value);
sendMessage(config.getRpcPort(dp.getChannel()), request);
}
/**
* Executes a script on the Homegear gateway.
*/
public void executeScript(HmDatapoint dp) throws IOException {
RpcRequest<T> request = createRpcRequest("runScript");
request.addArg(dp.getInfo());
sendMessage(config.getRpcPort(dp.getChannel()), request);
}
/**
* Enables/disables the install mode for given seconds.
*
* @param hmInterface specifies the interface to enable / disable install mode on
* @param enable if <i>true</i> it will be enabled, otherwise disabled
* @param seconds desired duration of install mode
* @throws IOException if RpcClient fails to propagate command
*/
public void setInstallMode(HmInterface hmInterface, boolean enable, int seconds) throws IOException {
RpcRequest<T> request = createRpcRequest("setInstallMode");
request.addArg(enable);
request.addArg(seconds);
request.addArg(INSTALL_MODE_NORMAL);
logger.debug("Submitting setInstallMode(on={}, time={}, mode={}) ", enable, seconds, INSTALL_MODE_NORMAL);
sendMessage(config.getRpcPort(hmInterface), request);
}
/**
* Returns the remaining time of <i>install_mode==true</i>
*
* @param hmInterface specifies the interface on which install mode status is requested
* @return current duration in seconds that the controller will remain in install mode,
* value of 0 means that the install mode is disabled
* @throws IOException if RpcClient fails to propagate command
*/
public int getInstallMode(HmInterface hmInterface) throws IOException {
RpcRequest<T> request = createRpcRequest("getInstallMode");
Object[] result = sendMessage(config.getRpcPort(hmInterface), request);
if (logger.isTraceEnabled()) {
logger.trace(
"Checking InstallMode: getInstallMode() request returned {} (remaining seconds in InstallMode=true)",
result);
}
try {
return (int) result[0];
} catch (Exception cause) {
IOException wrappedException = new IOException(
"Failed to request install mode from interface " + hmInterface);
wrappedException.initCause(cause);
throw wrappedException;
}
}
/**
* Deletes the device from the gateway.
*/
public void deleteDevice(HmDevice device, int flags) throws IOException {
RpcRequest<T> request = createRpcRequest("deleteDevice");
request.addArg(device.getAddress());
request.addArg(flags);
sendMessage(config.getRpcPort(device.getHmInterface()), request);
}
/**
* Returns the rpc address from a device address, correctly handling groups.
*/
private String getRpcAddress(String address) {
if (address != null && address.startsWith("T-")) {
address = "*" + address.substring(2);
}
return address;
}
/**
* Returns the rssi values for all devices.
*/
public List<HmRssiInfo> loadRssiInfo(HmInterface hmInterface) throws IOException {
RpcRequest<T> request = createRpcRequest("rssiInfo");
return new RssiInfoParser(config).parse(sendMessage(config.getRpcPort(hmInterface), request));
}
/**
* Returns the address suffix that specifies the channel for a given HmChannel. This is either a colon ":" followed
* by the channel number, or the empty string for a configuration channel.
*/
private String getChannelSuffix(HmChannel channel) {
return isConfigurationChannel(channel) ? "" : ":" + channel.getNumber();
}
/**
* Checks whether a channel is a configuration channel. The configuration channel of a device encapsulates the
* MASTER Paramset that does not belong to one of its actual channels.
*/
private boolean isConfigurationChannel(HmChannel channel) {
return channel.getNumber() == CONFIGURATION_CHANNEL_NUMBER;
}
}

View File

@@ -0,0 +1,101 @@
/**
* 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.homematic.internal.communicator.client;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Simple socket cache class.
*
* @author Gerhard Riegler - Initial contribution
*/
public class SocketHandler {
private final Logger logger = LoggerFactory.getLogger(SocketHandler.class);
private Map<Integer, SocketInfo> socketsPerPort = new HashMap<>();
private HomematicConfig config;
public SocketHandler(HomematicConfig config) {
this.config = config;
}
/**
* Returns a socket for the given port, (re)creates it if required.
*/
public Socket getSocket(int port) throws IOException {
SocketInfo socketInfo = socketsPerPort.get(port);
if (socketInfo == null) {
logger.trace("Creating new socket for port {}", port);
Socket socket = new Socket();
socket.setSoTimeout(config.getTimeout() * 1000);
socket.setReuseAddress(true);
socket.connect(new InetSocketAddress(config.getGatewayAddress(), port), socket.getSoTimeout());
socketInfo = new SocketInfo(socket);
socketsPerPort.put(port, socketInfo);
} else {
boolean isMaxAliveReached = System.currentTimeMillis()
- socketInfo.getCreated() > (config.getSocketMaxAlive() * 1000);
if (isMaxAliveReached) {
logger.debug("Max alive time reached for socket on port {}", port);
removeSocket(port);
return getSocket(port);
}
logger.trace("Returning socket for port {}", port);
}
return socketInfo.getSocket();
}
/**
* Removes the socket for the given port from the cache.
*/
public void removeSocket(int port) {
SocketInfo socketInfo = socketsPerPort.get(port);
if (socketInfo != null) {
logger.trace("Closing Socket on port {}", port);
socketsPerPort.remove(port);
closeSilent(socketInfo.getSocket());
}
}
/**
* Removes all cached sockets.
*/
public void flush() {
synchronized (SocketHandler.class) {
Integer[] portsToRemove = socketsPerPort.keySet().toArray(new Integer[0]);
for (Integer key : portsToRemove) {
removeSocket(key);
}
}
}
/**
* Silently closes the given socket.
*/
private void closeSilent(Socket socket) {
try {
socket.close();
} catch (IOException e) {
// ignore
}
}
}

View File

@@ -0,0 +1,44 @@
/**
* 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.homematic.internal.communicator.client;
import java.net.Socket;
/**
* Info class which holds some infos for caching a socket.
*
* @author Gerhard Riegler - Initial contribution
*/
public class SocketInfo {
private Socket socket;
private long created;
public SocketInfo(Socket socket) {
this.socket = socket;
this.created = System.currentTimeMillis();
}
/**
* Returns the socket.
*/
public Socket getSocket() {
return socket;
}
/**
* Returns the timestamp when the socket has been created.
*/
public long getCreated() {
return created;
}
}

View File

@@ -0,0 +1,23 @@
/**
* 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.homematic.internal.communicator.client;
/**
* Enumeration for available transfer modes.
*
* @author Gerhard Riegler - Initial contribution
*/
public enum TransferMode {
XML_RPC,
BIN_RPC
}

View File

@@ -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.homematic.internal.communicator.client;
import java.io.IOException;
/**
* Exception if the RPC call returns a unknown -3 Unknown paramset.
*
* @author Gerhard Riegler - Initial contribution
*/
public class UnknownParameterSetException extends IOException {
private static final long serialVersionUID = -246970996431236583L;
public UnknownParameterSetException(String message) {
super(message);
}
}

View File

@@ -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.homematic.internal.communicator.client;
import java.io.IOException;
/**
* Exception if the RPC call returns a unknown -1 Failure.
*
* @author Gerhard Riegler - Initial contribution
*/
public class UnknownRpcFailureException extends IOException {
private static final long serialVersionUID = -5695414238422364040L;
public UnknownRpcFailureException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,151 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.homematic.internal.communicator.client;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.util.BytesContentProvider;
import org.eclipse.jetty.client.util.InputStreamResponseListener;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.message.RpcRequest;
import org.openhab.binding.homematic.internal.communicator.message.XmlRpcRequest;
import org.openhab.binding.homematic.internal.communicator.message.XmlRpcResponse;
import org.openhab.binding.homematic.internal.communicator.parser.RpcResponseParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
/**
* Client implementation for sending messages via XML-RPC to the Homematic server.
*
* @author Gerhard Riegler - Initial contribution
*/
public class XmlRpcClient extends RpcClient<String> {
private final Logger logger = LoggerFactory.getLogger(XmlRpcClient.class);
private HttpClient httpClient;
public XmlRpcClient(HomematicConfig config, HttpClient httpClient) throws IOException {
super(config);
this.httpClient = httpClient;
}
@Override
public void dispose() {
}
@Override
public RpcRequest<String> createRpcRequest(String methodName) {
return new XmlRpcRequest(methodName);
}
/**
* Returns the XML-RPC url.
*/
@Override
protected String getRpcCallbackUrl() {
return "http://" + config.getCallbackHost() + ":" + config.getXmlCallbackPort();
}
@Override
protected synchronized Object[] sendMessage(int port, RpcRequest<String> request) throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("Client XmlRpcRequest (port {}):\n{}", port, request);
}
IOException reason = new IOException();
for (int rpcRetryCounter = 1; rpcRetryCounter <= MAX_RPC_RETRY; rpcRetryCounter++) {
try {
byte[] response = send(port, request);
if (response.length == 0 && "setInstallMode".equals(request.getMethodName())) {
return new Object[] {};
}
Object[] data = new XmlRpcResponse(new ByteArrayInputStream(response), config.getEncoding())
.getResponseData();
return new RpcResponseParser(request).parse(data);
} catch (UnknownRpcFailureException | UnknownParameterSetException ex) {
throw ex;
} catch (SAXException | ParserConfigurationException ex) {
throw new IOException(ex);
} catch (IOException ex) {
reason = ex;
if ("init".equals(request.getMethodName())) { // no retries for "init" request
break;
}
logger.debug("XmlRpcMessage failed, sending message again {}/{}", rpcRetryCounter, MAX_RPC_RETRY);
}
}
throw reason;
}
private byte[] send(int port, RpcRequest<String> request) throws IOException {
byte[] ret = new byte[0];
try {
BytesContentProvider content = new BytesContentProvider(
request.createMessage().getBytes(config.getEncoding()));
String url = String.format("http://%s:%s", config.getGatewayAddress(), port);
if (port == config.getGroupPort()) {
url += "/groups";
}
Request req = httpClient.POST(url).content(content).timeout(config.getTimeout(), TimeUnit.SECONDS)
.header(HttpHeader.CONTENT_TYPE, "text/xml;charset=" + config.getEncoding());
try {
ret = req.send().getContent();
} catch (IllegalArgumentException e) { // Returned buffer too large
logger.info("Blocking XmlRpcRequest failed: {}, trying non-blocking request", e.getMessage());
InputStreamResponseListener respListener = new InputStreamResponseListener();
req.send(respListener);
Response resp = respListener.get(config.getTimeout(), TimeUnit.SECONDS);
ByteArrayOutputStream respData = new ByteArrayOutputStream(RESP_BUFFER_SIZE);
int httpStatus = resp.getStatus();
if (httpStatus == HttpStatus.OK_200) {
byte[] recvBuffer = new byte[RESP_BUFFER_SIZE];
try (InputStream input = respListener.getInputStream()) {
while (true) {
int read = input.read(recvBuffer);
if (read == -1) {
break;
}
respData.write(recvBuffer, 0, read);
}
ret = respData.toByteArray();
}
} else {
logger.warn("Non-blocking XmlRpcRequest failed, status code: {} / request: {}", httpStatus,
request);
resp.abort(new IOException());
}
}
if (logger.isTraceEnabled()) {
String result = new String(ret, config.getEncoding());
logger.trace("Client XmlRpcResponse (port {}):\n{}", port, result);
}
} catch (UnsupportedEncodingException | ExecutionException | TimeoutException | InterruptedException e) {
throw new IOException(e);
}
return ret;
}
}

View File

@@ -0,0 +1,390 @@
/**
* 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.homematic.internal.communicator.message;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.lang.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles BIN-RPC request and response messages to communicate with a Homematic gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public class BinRpcMessage implements RpcRequest<byte[]>, RpcResponse {
private final Logger logger = LoggerFactory.getLogger(BinRpcMessage.class);
public enum TYPE {
REQUEST,
RESPONSE
}
private Object[] messageData;
private byte binRpcData[];
private int offset;
private String methodName;
private TYPE type;
private int args;
private String encoding;
public BinRpcMessage(String methodName, String encoding) {
this(methodName, TYPE.REQUEST, encoding);
}
/**
* Creates a new request with the specified methodName.
*/
public BinRpcMessage(String methodName, TYPE type, String encoding) {
this.methodName = methodName;
this.type = type;
this.encoding = encoding;
createHeader();
}
/**
* Decodes a BIN-RPC message from the given InputStream.
*/
public BinRpcMessage(InputStream is, boolean methodHeader, String encoding) throws IOException {
this.encoding = encoding;
byte sig[] = new byte[8];
int length = is.read(sig, 0, 4);
if (length != 4) {
throw new EOFException("Only " + length + " bytes received reading signature");
}
validateBinXSignature(sig);
length = is.read(sig, 4, 4);
if (length != 4) {
throw new EOFException("Only " + length + " bytes received reading message length");
}
int datasize = (new BigInteger(ArrayUtils.subarray(sig, 4, 8))).intValue();
byte payload[] = new byte[datasize];
int offset = 0;
int currentLength;
while (offset < datasize && (currentLength = is.read(payload, offset, datasize - offset)) != -1) {
offset += currentLength;
}
if (offset != datasize) {
throw new EOFException("Only " + offset + " bytes received while reading message payload, expected "
+ datasize + " bytes");
}
byte[] message = ArrayUtils.addAll(sig, payload);
decodeMessage(message, methodHeader);
}
private void validateBinXSignature(byte[] sig) throws UnsupportedEncodingException {
if (sig[0] != 'B' || sig[1] != 'i' || sig[2] != 'n') {
throw new UnsupportedEncodingException("No BinX signature");
}
}
/**
* Decodes a BIN-RPC message from the given byte array.
*/
public BinRpcMessage(byte[] message, boolean methodHeader, String encoding) throws IOException, ParseException {
this.encoding = encoding;
if (message.length < 8) {
throw new EOFException("Only " + message.length + " bytes received");
}
validateBinXSignature(message);
decodeMessage(message, methodHeader);
}
private void decodeMessage(byte[] message, boolean methodHeader) throws IOException {
binRpcData = message;
offset = 8;
if (methodHeader) {
methodName = readString();
readInt();
}
generateResponseData();
}
public void setType(TYPE type) {
binRpcData[3] = type == TYPE.RESPONSE ? (byte) 1 : (byte) 0;
}
private void generateResponseData() throws IOException {
offset = 8 + (methodName != null ? methodName.length() + 8 : 0);
List<Object> values = new ArrayList<>();
while (offset < binRpcData.length) {
values.add(readRpcValue());
}
messageData = values.toArray();
values.clear();
}
private void createHeader() {
binRpcData = new byte[256];
addString("Bin ");
setType(type);
addInt(0); // placeholder content length
if (methodName != null) {
addInt(methodName.length());
addString(methodName);
addInt(0); // placeholder arguments
}
setInt(4, offset - 8);
}
/**
* Adds arguments to the method.
*/
@Override
public void addArg(Object argument) {
addObject(argument);
setInt(4, offset - 8);
if (methodName != null) {
setInt(12 + methodName.length(), ++args);
}
}
public int getArgCount() {
return args;
}
@Override
public String getMethodName() {
return methodName;
}
@Override
public byte[] createMessage() {
trimBinRpcData();
return binRpcData;
}
private void trimBinRpcData() {
byte[] trimmed = new byte[offset];
System.arraycopy(binRpcData, 0, trimmed, 0, offset);
binRpcData = trimmed;
}
@Override
public Object[] getResponseData() {
return messageData;
}
// read rpc values
private int readInt() {
byte bi[] = new byte[4];
System.arraycopy(binRpcData, offset, bi, 0, 4);
offset += 4;
return (new BigInteger(bi)).intValue();
}
private long readInt64() {
byte bi[] = new byte[8];
System.arraycopy(binRpcData, offset, bi, 0, 8);
offset += 8;
return (new BigInteger(bi)).longValue();
}
private String readString() throws UnsupportedEncodingException {
int len = readInt();
offset += len;
return new String(binRpcData, offset - len, len, encoding);
}
private Object readRpcValue() throws IOException {
int type = readInt();
switch (type) {
case 1:
return new Integer(readInt());
case 2:
return binRpcData[offset++] != 0 ? Boolean.TRUE : Boolean.FALSE;
case 3:
return readString();
case 4:
int mantissa = readInt();
int exponent = readInt();
BigDecimal bd = new BigDecimal((double) mantissa / (double) (1 << 30) * Math.pow(2, exponent));
return bd.setScale(6, RoundingMode.HALF_DOWN).doubleValue();
case 5:
return new Date(readInt() * 1000);
case 0xD1:
// Int64
return new Long(readInt64());
case 0x100:
// Array
int numElements = readInt();
Collection<Object> array = new ArrayList<>();
while (numElements-- > 0) {
array.add(readRpcValue());
}
return array.toArray();
case 0x101:
// Struct
numElements = readInt();
Map<String, Object> struct = new TreeMap<>();
while (numElements-- > 0) {
String name = readString();
struct.put(name, readRpcValue());
}
return struct;
default:
for (int i = 0; i < binRpcData.length; i++) {
logger.info("{} {}", Integer.toHexString(binRpcData[i]), (char) binRpcData[i]);
}
throw new IOException("Unknown data type " + type);
}
}
private void setInt(int position, int value) {
int temp = offset;
offset = position;
addInt(value);
offset = temp;
}
private void addByte(byte b) {
if (offset == binRpcData.length) {
byte newdata[] = new byte[binRpcData.length * 2];
System.arraycopy(binRpcData, 0, newdata, 0, binRpcData.length);
binRpcData = newdata;
}
binRpcData[offset++] = b;
}
private void addInt(int value) {
addByte((byte) (value >> 24));
addByte((byte) (value >> 16));
addByte((byte) (value >> 8));
addByte((byte) (value));
}
private void addDouble(double value) {
double tmp = Math.abs(value);
int exp = 0;
if (tmp != 0 && tmp < 0.5) {
while (tmp < 0.5) {
tmp *= 2;
exp--;
}
} else {
while (tmp >= 1) {
tmp /= 2;
exp++;
}
}
if (value < 0) {
tmp *= -1;
}
int mantissa = (int) Math.round(tmp * 0x40000000);
addInt(mantissa);
addInt(exp);
}
private void addString(String string) {
byte sd[];
try {
sd = string.getBytes(encoding);
} catch (UnsupportedEncodingException use) {
sd = string.getBytes();
}
for (byte ch : sd) {
addByte(ch);
}
}
private void addList(Collection<?> collection) {
for (Object object : collection) {
addObject(object);
}
}
private void addObject(Object object) {
if (object.getClass() == String.class) {
addInt(3);
String string = (String) object;
addInt(string.length());
addString(string);
} else if (object.getClass() == Boolean.class) {
addInt(2);
addByte(((Boolean) object).booleanValue() ? (byte) 1 : (byte) 0);
} else if (object.getClass() == Integer.class) {
addInt(1);
addInt(((Integer) object).intValue());
} else if (object.getClass() == Double.class) {
addInt(4);
addDouble(((Double) object).doubleValue());
} else if (object.getClass() == Float.class) {
addInt(4);
BigDecimal bd = new BigDecimal((Float) object);
addDouble(bd.setScale(6, RoundingMode.HALF_DOWN).doubleValue());
} else if (object.getClass() == BigDecimal.class) {
addInt(4);
addDouble(((BigDecimal) object).setScale(6, RoundingMode.HALF_DOWN).doubleValue());
} else if (object.getClass() == BigInteger.class) {
addInt(4);
addDouble(((BigInteger) object).doubleValue());
} else if (object.getClass() == Date.class) {
addInt(5);
addInt((int) ((Date) object).getTime() / 1000);
} else if (object instanceof List<?>) {
Collection<?> list = (Collection<?>) object;
addInt(0x100);
addInt(list.size());
addList(list);
} else if (object instanceof Map<?, ?>) {
Map<?, ?> map = (Map<?, ?>) object;
addInt(0x101);
addInt(map.size());
for (Map.Entry<?, ?> entry : map.entrySet()) {
String key = (String) entry.getKey();
if (key != null) {
addInt(key.length());
addString(key);
addList(Collections.singleton(entry.getValue()));
}
}
}
}
public String toBinString() {
return Arrays.toString(createMessage());
}
@Override
public String toString() {
try {
trimBinRpcData();
generateResponseData();
return RpcUtils.dumpRpcMessage(methodName, messageData);
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}

View File

@@ -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.homematic.internal.communicator.message;
/**
* A RPC request definition for sending data to the Homematic server.
*
* @author Gerhard Riegler - Initial contribution
*/
public interface RpcRequest<T> {
/**
* Adds arguments to the RPC method.
*/
public void addArg(Object arg);
/**
* Generates the RPC data.
*/
public T createMessage();
/**
* Returns the name of the rpc method.
*/
public String getMethodName();
}

View File

@@ -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.homematic.internal.communicator.message;
/**
* A RPC response definition for reveiving data from the Homematic server.
*
* @author Gerhard Riegler - Initial contribution
*/
public interface RpcResponse {
/**
* Returns the decoded methodName.
*/
public String getMethodName();
/**
* Returns the decoded data.
*/
public Object[] getResponseData();
}

View File

@@ -0,0 +1,97 @@
/**
* 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.homematic.internal.communicator.message;
import java.util.Map;
/**
* Helper class with common RPC funtions.
*
* @author Gerhard Riegler - Initial contribution
*/
public class RpcUtils {
/**
* Dumps decoded RPC data.
*/
public static String dumpRpcMessage(String methodName, Object[] responseData) {
StringBuilder sb = new StringBuilder();
if (methodName != null) {
sb.append(methodName);
sb.append("()\n");
}
dumpCollection(responseData, sb, 0);
return sb.toString();
}
private static void dumpCollection(Object[] c, StringBuilder sb, int indent) {
if (indent > 0) {
for (int in = 0; in < indent - 1; in++) {
sb.append('\t');
}
sb.append("[\n");
}
for (Object o : c) {
if (o instanceof Map) {
dumpMap((Map<?, ?>) o, sb, indent + 1);
} else if (o instanceof Object[]) {
dumpCollection((Object[]) o, sb, indent + 1);
} else {
for (int in = 0; in < indent; in++) {
sb.append('\t');
}
sb.append(o);
sb.append('\n');
}
}
if (indent > 0) {
for (int in = 0; in < indent - 1; in++) {
sb.append('\t');
}
sb.append("]\n");
}
}
private static void dumpMap(Map<?, ?> c, StringBuilder sb, int indent) {
if (indent > 0) {
for (int in = 0; in < indent - 1; in++) {
sb.append('\t');
}
sb.append("{\n");
}
for (Map.Entry<?, ?> me : c.entrySet()) {
Object o = me.getValue();
for (int in = 0; in < indent; in++) {
sb.append('\t');
}
sb.append(me.getKey());
sb.append('=');
if (o instanceof Map<?, ?>) {
sb.append("\n");
dumpMap((Map<?, ?>) o, sb, indent + 1);
} else if (o instanceof Object[]) {
sb.append("\n");
dumpCollection((Object[]) o, sb, indent + 1);
} else {
sb.append(o);
sb.append('\n');
}
}
if (indent > 0) {
for (int in = 0; in < indent - 1; in++) {
sb.append('\t');
}
sb.append("}\n");
}
}
}

View File

@@ -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.homematic.internal.communicator.message;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.lang.StringEscapeUtils;
/**
* A XML-RPC request for sending data to the Homematic server.
*
* @author Gerhard Riegler - Initial contribution
*/
public class XmlRpcRequest implements RpcRequest<String> {
public enum TYPE {
REQUEST,
RESPONSE
}
private String methodName;
private List<Object> parms;
private StringBuilder sb;
private TYPE type;
public static SimpleDateFormat xmlRpcDateFormat = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
public XmlRpcRequest(String methodName) {
this(methodName, TYPE.REQUEST);
}
public XmlRpcRequest(String methodName, TYPE type) {
this.methodName = methodName;
this.type = type;
parms = new ArrayList<>();
}
@Override
public void addArg(Object parameter) {
parms.add(parameter);
}
@Override
public String createMessage() {
return toString();
}
@Override
public String getMethodName() {
return methodName;
}
@Override
public String toString() {
sb = new StringBuilder();
sb.append("<?xml");
attr("version", "1.0");
attr("encoding", "ISO-8859-1");
sb.append("?>\n");
if (type == TYPE.REQUEST) {
sb.append("<methodCall>");
tag("methodName", methodName);
} else {
sb.append("<methodResponse>");
}
sb.append("\n");
sb.append("<params>");
for (Object parameter : parms) {
sb.append("<param><value>");
generateValue(parameter);
sb.append("</value></param>");
}
sb.append("</params>");
if (type == TYPE.REQUEST) {
sb.append("</methodCall>");
} else {
sb.append("</methodResponse>");
}
return sb.toString();
}
/**
* Generates a XML attribute.
*/
private void attr(String name, String value) {
sb.append(" ").append(name).append("=\"").append(value).append("\"");
}
/**
* Generates a XML tag.
*/
private void tag(String name, String value) {
sb.append("<").append(name).append(">").append(value).append("</").append(name).append(">");
}
/**
* Generates a value tag based on the type of the value.
*/
private void generateValue(Object value) {
if (value == null) {
tag("string", "void");
} else {
Class<?> clazz = value.getClass();
if (clazz == String.class || clazz == Character.class) {
sb.append(StringEscapeUtils.escapeXml(value.toString()));
} else if (clazz == Long.class || clazz == Integer.class || clazz == Short.class || clazz == Byte.class) {
tag("int", value.toString());
} else if (clazz == Double.class) {
tag("double", String.valueOf(((Double) value).doubleValue()));
} else if (clazz == Float.class) {
BigDecimal bd = new BigDecimal((Float) value);
generateValue(bd.setScale(6, RoundingMode.HALF_DOWN).doubleValue());
} else if (clazz == BigDecimal.class) {
generateValue(((BigDecimal) value).setScale(6, RoundingMode.HALF_DOWN).doubleValue());
} else if (clazz == Boolean.class) {
tag("boolean", ((Boolean) value).booleanValue() ? "1" : "0");
} else if (clazz == Date.class) {
tag("dateTime.iso8601", xmlRpcDateFormat.format(((Date) value)));
} else if (value instanceof Calendar) {
generateValue(((Calendar) value).getTime());
} else if (value instanceof byte[]) {
tag("base64", Base64.getEncoder().encodeToString((byte[]) value));
} else if (clazz.isArray() || value instanceof List) {
sb.append("<array><data>");
Object[] array = null;
if (value instanceof List) {
array = ((List<?>) value).toArray();
} else {
array = (Object[]) value;
}
for (Object arrayObject : array) {
sb.append("<value>");
generateValue(arrayObject);
sb.append("</value>");
}
sb.append("</data></array>");
} else if (value instanceof Map) {
sb.append("<struct>");
for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
sb.append("<member>");
sb.append("<name>").append(entry.getKey()).append("</name>");
sb.append("<value>");
generateValue(entry.getValue());
sb.append("</value>");
sb.append("</member>");
}
sb.append("</struct>");
} else {
throw new RuntimeException("Unsupported XML-RPC Type: " + value.getClass());
}
}
}
}

View File

@@ -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.homematic.internal.communicator.message;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Decodes a XML-RPC message from the Homematic server.
*
* @author Gerhard Riegler - Initial contribution
*/
public class XmlRpcResponse implements RpcResponse {
private String methodName;
private Object[] responseData;
/**
* Decodes a XML-RPC message from the given InputStream.
*/
public XmlRpcResponse(InputStream is, String encoding)
throws SAXException, ParserConfigurationException, IOException {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
InputSource inputSource = new InputSource(is);
inputSource.setEncoding(encoding);
saxParser.parse(inputSource, new XmlRpcHandler());
}
@Override
public Object[] getResponseData() {
return responseData;
}
@Override
public String getMethodName() {
return methodName;
}
@Override
public String toString() {
return RpcUtils.dumpRpcMessage(methodName, responseData);
}
/**
* SAX parser implementation to decode XML-RPC.
*
* @author Gerhard Riegler
*/
private class XmlRpcHandler extends DefaultHandler {
private List<Object> result = new ArrayList<>();
private LinkedList<List<Object>> currentDataObject = new LinkedList<>();
private StringBuilder tagValue;
private boolean isValueTag;
@Override
public void startDocument() throws SAXException {
currentDataObject.addLast(new ArrayList<>());
}
@Override
public void endDocument() throws SAXException {
result.addAll(currentDataObject.removeLast());
responseData = result.toArray();
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
String tag = qName.toLowerCase();
if (tag.equals("array") || tag.equals("struct")) {
currentDataObject.addLast(new ArrayList<>());
}
isValueTag = tag.equals("value");
tagValue = new StringBuilder();
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
String currentTag = qName.toLowerCase();
String currentValue = tagValue.toString();
List<Object> data = currentDataObject.peekLast();
switch (currentTag) {
case "boolean":
data.add("1".equals(currentValue) ? Boolean.TRUE : Boolean.FALSE);
break;
case "int":
case "i4":
data.add(new Integer(currentValue));
break;
case "double":
data.add(new Double(currentValue));
break;
case "string":
case "name":
data.add(currentValue);
break;
case "value":
if (isValueTag) {
data.add(currentValue);
isValueTag = false;
}
break;
case "array":
List<Object> arrayData = currentDataObject.removeLast();
currentDataObject.peekLast().add(arrayData.toArray());
break;
case "struct":
List<Object> mapData = currentDataObject.removeLast();
Map<Object, Object> resultMap = new HashMap<>();
for (int i = 0; i < mapData.size(); i += 2) {
resultMap.put(mapData.get(i), mapData.get(i + 1));
}
currentDataObject.peekLast().add(resultMap);
break;
case "base64":
data.add(Base64.getDecoder().decode(currentValue));
break;
case "datetime.iso8601":
try {
data.add(XmlRpcRequest.xmlRpcDateFormat.parse(currentValue));
} catch (ParseException ex) {
throw new SAXException(ex.getMessage(), ex);
}
break;
case "methodname":
methodName = currentValue;
break;
case "params":
case "param":
case "methodcall":
case "methodresponse":
case "member":
case "data":
case "fault":
break;
default:
throw new SAXException("Unknown XML-RPC tag: " + currentTag);
}
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
tagValue.append(new String(ch, start, length));
}
}
}

View File

@@ -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.homematic.internal.communicator.parser;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.TclScriptDataEntry;
import org.openhab.binding.homematic.internal.model.TclScriptDataList;
/**
* Parses a TclRega script result containing names for devices.
*
* @author Gerhard Riegler - Initial contribution
*/
public class CcuLoadDeviceNamesParser extends CommonRpcParser<TclScriptDataList, Void> {
private Collection<HmDevice> devices;
public CcuLoadDeviceNamesParser(Collection<HmDevice> devices) {
this.devices = devices;
}
@Override
public Void parse(TclScriptDataList resultList) throws IOException {
if (resultList.getEntries() != null) {
Map<String, HmDevice> devicesByAddress = new HashMap<>();
for (HmDevice device : devices) {
devicesByAddress.put(device.getAddress(), device);
}
for (TclScriptDataEntry entry : resultList.getEntries()) {
HmDevice device = devicesByAddress.get(getSanitizedAddress(entry.name));
if (device != null) {
device.setName(entry.value);
}
}
}
return null;
}
}

View File

@@ -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.homematic.internal.communicator.parser;
import java.io.IOException;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmInterface;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.openhab.binding.homematic.internal.model.TclScriptDataEntry;
import org.openhab.binding.homematic.internal.model.TclScriptDataList;
/**
* Parses parameter descriptions from a CCU script and extracts datapoint metadata.
*
* @author Gerhard Riegler - Initial contribution
*/
public class CcuParamsetDescriptionParser extends CommonRpcParser<TclScriptDataList, Void> {
private HmParamsetType paramsetType;
private HmChannel channel;
private boolean isHmIpDevice;
public CcuParamsetDescriptionParser(HmChannel channel, HmParamsetType paramsetType) {
this.channel = channel;
this.paramsetType = paramsetType;
this.isHmIpDevice = channel.getDevice().getHmInterface() == HmInterface.HMIP;
}
@Override
public Void parse(TclScriptDataList resultList) throws IOException {
if (resultList.getEntries() != null) {
for (TclScriptDataEntry entry : resultList.getEntries()) {
HmDatapoint dp = assembleDatapoint(entry.name, entry.unit, entry.valueType,
this.toOptionList(entry.options), convertToType(entry.minValue), convertToType(entry.maxValue),
toInteger(entry.operations), convertToType(entry.value), paramsetType, isHmIpDevice);
channel.addDatapoint(dp);
}
}
return null;
}
private String[] toOptionList(String options) {
String[] result = StringUtils.splitByWholeSeparatorPreserveAllTokens(options, ";");
return result == null || result.length == 0 ? null : result;
}
}

View File

@@ -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.homematic.internal.communicator.parser;
import java.io.IOException;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.binding.homematic.internal.model.TclScriptDataEntry;
import org.openhab.binding.homematic.internal.model.TclScriptDataList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Parses a TclRega script result containing datapoint values for a channel.
*
* @author Gerhard Riegler - Initial contribution
*/
public class CcuValueParser extends CommonRpcParser<TclScriptDataList, Void> {
private final Logger logger = LoggerFactory.getLogger(CcuValueParser.class);
private HmChannel channel;
public CcuValueParser(HmChannel channel) {
this.channel = channel;
}
@Override
public Void parse(TclScriptDataList resultList) throws IOException {
if (resultList.getEntries() != null) {
for (TclScriptDataEntry entry : resultList.getEntries()) {
HmDatapointInfo dpInfo = HmDatapointInfo.createValuesInfo(channel, entry.name);
HmDatapoint dp = channel.getDatapoint(dpInfo);
if (dp != null) {
dp.setValue(convertToType(dp, entry.value));
adjustRssiValue(dp);
} else {
// should never happen, but in case ...
logger.warn("Can't set value for datapoint '{}'", dpInfo);
}
}
}
return null;
}
}

View File

@@ -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.homematic.internal.communicator.parser;
import java.io.IOException;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.openhab.binding.homematic.internal.model.HmValueType;
import org.openhab.binding.homematic.internal.model.TclScriptDataEntry;
import org.openhab.binding.homematic.internal.model.TclScriptDataList;
/**
* Parses a TclRega script result containing variables and scripts.
*
* @author Gerhard Riegler - Initial contribution
*/
public class CcuVariablesAndScriptsParser extends CommonRpcParser<TclScriptDataList, Void> {
private HmChannel channel;
public CcuVariablesAndScriptsParser(HmChannel channel) {
this.channel = channel;
}
@Override
public Void parse(TclScriptDataList resultList) throws IOException {
if (resultList.getEntries() != null) {
for (TclScriptDataEntry entry : resultList.getEntries()) {
HmDatapoint dp = channel.getDatapoint(HmParamsetType.VALUES, entry.name);
if (dp != null) {
dp.setValue(convertToType(entry.value));
} else {
dp = new HmDatapoint();
dp.setName(entry.name);
dp.setInfo(entry.name);
dp.setDescription(entry.description);
dp.setType(HmValueType.parse(entry.valueType));
dp.setValue(convertToType(entry.value));
if (dp.isIntegerType()) {
dp.setMinValue(toInteger(entry.minValue));
dp.setMaxValue(toInteger(entry.maxValue));
} else {
dp.setMinValue(toDouble(entry.minValue));
dp.setMaxValue(toDouble(entry.maxValue));
}
dp.setReadOnly(entry.readOnly);
dp.setUnit(entry.unit);
String[] result = StringUtils.splitByWholeSeparatorPreserveAllTokens(entry.options, ";");
dp.setOptions(result == null || result.length == 0 ? null : result);
if (dp.getOptions() != null) {
dp.setMinValue(0);
dp.setMaxValue(dp.getOptions().length - 1);
}
dp.setParamsetType(HmParamsetType.VALUES);
channel.addDatapoint(dp);
}
}
}
return null;
}
}

View File

@@ -0,0 +1,228 @@
/**
* 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.homematic.internal.communicator.parser;
import java.io.IOException;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.openhab.binding.homematic.internal.misc.MiscUtils;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.openhab.binding.homematic.internal.model.HmValueType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract base class for all parsers with common methods.
*
* @author Gerhard Riegler - Initial contribution
*/
public abstract class CommonRpcParser<M, R> implements RpcParser<M, R> {
private final Logger logger = LoggerFactory.getLogger(CommonRpcParser.class);
/**
* Converts the object to a string.
*/
protected String toString(Object object) {
return StringUtils.trimToNull(ObjectUtils.toString(object));
}
/**
* Converts the object to a integer.
*/
protected Integer toInteger(Object object) {
if (object == null || object instanceof Integer) {
return (Integer) object;
}
try {
return Double.valueOf(ObjectUtils.toString(object)).intValue();
} catch (NumberFormatException ex) {
logger.debug("Failed converting {} to a Double", object, ex);
return null;
}
}
/**
* Converts the object to a double.
*/
protected Double toDouble(Object object) {
if (object == null || object instanceof Double) {
return (Double) object;
}
try {
return Double.valueOf(ObjectUtils.toString(object));
} catch (NumberFormatException ex) {
logger.debug("Failed converting {} to a Double", object, ex);
return null;
}
}
/**
* Converts the object to a number.
*/
protected Number toNumber(Object object) {
if (object == null || object instanceof Number) {
return (Number) object;
}
try {
return NumberUtils.createNumber(ObjectUtils.toString(object));
} catch (NumberFormatException ex) {
logger.debug("Failed converting {} to a Number", object, ex);
return null;
}
}
/**
* Converts the object to a boolean.
*/
protected Boolean toBoolean(Object object) {
if (object == null || object instanceof Boolean) {
return (Boolean) object;
}
return BooleanUtils.toBoolean(ObjectUtils.toString(object));
}
/**
* Converts the object to a string array.
*/
protected String[] toOptionList(Object optionList) {
if (optionList != null && optionList instanceof Object[]) {
Object[] vl = (Object[]) optionList;
String[] stringArray = new String[vl.length];
for (int i = 0; i < vl.length; i++) {
stringArray[i] = vl[i].toString();
}
return stringArray;
}
return null;
}
/**
* Returns the address of a device, replacing group address identifier and illegal characters.
*/
protected String getSanitizedAddress(Object object) {
String address = StringUtils.trimToNull(StringUtils.replaceOnce(toString(object), "*", "T-"));
return MiscUtils.validateCharacters(address, "Address", "_");
}
/**
* Adjust uninitialized rssi values to zero.
*/
protected void adjustRssiValue(HmDatapoint dp) {
if (dp.getValue() != null && dp.getName().startsWith("RSSI_") && dp.isIntegerType()) {
int rssiValue = ((Number) dp.getValue()).intValue();
dp.setValue(getAdjustedRssiValue(rssiValue));
}
}
/**
* Adjust a rssi value if it is out of range.
*/
protected Integer getAdjustedRssiValue(Integer rssiValue) {
if (rssiValue == null || rssiValue >= 255 || rssiValue <= -255) {
return 0;
}
return rssiValue;
}
/**
* Converts the value to the correct type if necessary.
*/
protected Object convertToType(HmDatapoint dp, Object value) {
if (value == null) {
return null;
} else if (dp.isBooleanType()) {
return toBoolean(value);
} else if (dp.isIntegerType()) {
return toInteger(value);
} else if (dp.isFloatType()) {
return toNumber(value);
} else if (dp.isStringType()) {
return toString(value);
} else {
return value;
}
}
/**
* Assembles a datapoint with the given parameters.
*/
protected HmDatapoint assembleDatapoint(String name, String unit, String type, String[] options, Object min,
Object max, Integer operations, Object defaultValue, HmParamsetType paramsetType, boolean isHmIpDevice)
throws IOException {
HmDatapoint dp = new HmDatapoint();
dp.setName(name);
dp.setDescription(name);
dp.setUnit(StringUtils.replace(StringUtils.trimToNull(unit), "\ufffd", "°"));
if (dp.getUnit() == null && StringUtils.startsWith(dp.getName(), "RSSI_")) {
dp.setUnit("dBm");
}
HmValueType valueType = HmValueType.parse(type);
if (valueType == null || valueType == HmValueType.UNKNOWN) {
throw new IOException("Unknown datapoint type: " + type);
} else if (valueType == HmValueType.FLOAT && dp.getUnit() == null
&& dp.getName().matches("\\w*_TEMPERATURE(_\\w.*|$)")) {
logger.debug("No unit information found for temperature datapoint {}, assuming Number:Temperature",
dp.getName());
dp.setUnit("°C"); // Bypass for a problem with HMIP devices where unit of temperature channels is sometimes
// empty
}
dp.setType(valueType);
dp.setOptions(options);
if (dp.isNumberType() || dp.isEnumType()) {
if (isHmIpDevice && dp.isEnumType()) {
dp.setMinValue(dp.getOptionIndex(toString(min)));
dp.setMaxValue(dp.getOptionIndex(toString(max)));
} else {
dp.setMinValue(toNumber(min));
dp.setMaxValue(toNumber(max));
}
}
dp.setReadOnly((operations & 2) != 2);
dp.setReadable((operations & 1) == 1);
dp.setParamsetType(paramsetType);
if (isHmIpDevice && dp.isEnumType()) {
dp.setDefaultValue(dp.getOptionIndex(toString(defaultValue)));
} else {
dp.setDefaultValue(convertToType(dp, defaultValue));
}
dp.setValue(dp.getDefaultValue());
return dp;
}
/**
* Converts a string value to the type.
*/
protected Object convertToType(String value) {
if (StringUtils.isBlank(value)) {
return null;
}
if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("on")) {
return (Boolean.TRUE);
} else if (value.equalsIgnoreCase("false") || value.equalsIgnoreCase("off")) {
return (Boolean.FALSE);
} else if (value.matches("(-|\\+)?[0-9]+")) {
return (Integer.valueOf(value));
} else if (value.matches("[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?")) {
return (Double.valueOf(value));
} else {
return value;
}
}
}

View File

@@ -0,0 +1,44 @@
/**
* 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.homematic.internal.communicator.parser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
/**
* Parses a delete device event received from a Homematic gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public class DeleteDevicesParser extends CommonRpcParser<Object[], List<String>> {
@Override
public List<String> parse(Object[] message) throws IOException {
List<String> adresses = new ArrayList<>();
if (message != null && message.length > 1) {
Object[] data = (Object[]) message[1];
for (int i = 0; i < message.length; i++) {
String address = getSanitizedAddress(data[i]);
boolean isDevice = !StringUtils.contains(address, ":")
&& !StringUtils.startsWithIgnoreCase(address, "BidCos");
if (isDevice) {
adresses.add(address);
}
}
}
return adresses;
}
}

View File

@@ -0,0 +1,190 @@
/**
* 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.homematic.internal.communicator.parser;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Extracts the possible options from the remote control metadata and parses the DISPLAY_OPTIONS virtual datapoint.
*
* @author Gerhard Riegler - Initial contribution
*/
public class DisplayOptionsParser extends CommonRpcParser<Object, Void> {
private final Logger logger = LoggerFactory.getLogger(DisplayOptionsParser.class);
private static final String[] onOff = new String[] { "ON", "OFF" };
private HmChannel channel;
private String text;
private int beep = 0;
private int backlight = 0;
private int unit = 0;
private List<String> symbols = new ArrayList<>();
public DisplayOptionsParser(HmChannel channel) {
this.channel = channel;
}
@Override
public Void parse(Object value) throws IOException {
String optionsString = StringUtils.remove(toString(value), ' ');
if (optionsString != null) {
int idxFirstSep = optionsString.indexOf(",");
if (idxFirstSep == -1) {
text = optionsString;
optionsString = "";
} else {
text = optionsString.substring(0, idxFirstSep);
optionsString = optionsString.substring(idxFirstSep + 1);
}
String[] options = StringUtils.split(optionsString, ",");
String[] availableSymbols = getAvailableSymbols(channel);
String[] availableBeepOptions = getAvailableOptions(channel, DATAPOINT_NAME_BEEP);
String[] availableBacklightOptions = getAvailableOptions(channel, DATAPOINT_NAME_BACKLIGHT);
String[] availableUnitOptions = getAvailableOptions(channel, DATAPOINT_NAME_UNIT);
String deviceAddress = channel.getDevice().getAddress();
if (logger.isDebugEnabled()) {
logger.debug("Remote control '{}' supports these beep options: {}", deviceAddress,
availableBeepOptions);
logger.debug("Remote control '{}' supports these backlight options: {}", deviceAddress,
availableBacklightOptions);
logger.debug("Remote control '{}' supports these unit options: {}", deviceAddress,
availableUnitOptions);
logger.debug("Remote control '{}' supports these symbols: {}", deviceAddress, symbols);
}
if (options != null) {
for (String parameter : options) {
logger.debug("Parsing remote control option '{}'", parameter);
beep = getIntParameter(availableBeepOptions, beep, parameter, DATAPOINT_NAME_BEEP, deviceAddress);
backlight = getIntParameter(availableBacklightOptions, backlight, parameter,
DATAPOINT_NAME_BACKLIGHT, deviceAddress);
unit = getIntParameter(availableUnitOptions, unit, parameter, DATAPOINT_NAME_UNIT, deviceAddress);
if (ArrayUtils.contains(availableSymbols, parameter)) {
logger.debug("Symbol '{}' found for remote control '{}'", parameter, deviceAddress);
symbols.add(parameter);
}
}
}
}
return null;
}
/**
* Returns the first found parameter index of the options.
*/
private int getIntParameter(String[] options, int currentValue, String parameter, String parameterName,
String deviceAddress) {
int idx = ArrayUtils.indexOf(options, parameter);
if (idx != -1) {
if (currentValue == 0) {
logger.debug("{} option '{}' found at index {} for remote control '{}'", parameterName, parameter,
idx + 1, deviceAddress);
return idx + 1;
} else {
logger.warn("{} option already set for remote control '{}', ignoring '{}'!", parameterName,
deviceAddress, parameter);
return currentValue;
}
} else {
return currentValue;
}
}
/**
* Returns all possible options from the given datapoint.
*/
private String[] getAvailableOptions(HmChannel channel, String datapointName) {
HmDatapointInfo dpInfo = HmDatapointInfo.createValuesInfo(channel, datapointName);
HmDatapoint dp = channel.getDatapoint(dpInfo);
if (dp != null) {
String[] options = (String[]) ArrayUtils.remove(dp.getOptions(), 0);
for (String onOffString : onOff) {
int onIdx = ArrayUtils.indexOf(options, onOffString);
if (onIdx != -1) {
options[onIdx] = datapointName + "_" + onOffString;
}
}
return options;
}
return new String[0];
}
/**
* Returns all possible symbols from the remote control.
*/
private String[] getAvailableSymbols(HmChannel channel) {
List<String> symbols = new ArrayList<>();
for (HmDatapoint datapoint : channel.getDatapoints()) {
if (!datapoint.isReadOnly() && datapoint.isBooleanType()
&& datapoint.getParamsetType() == HmParamsetType.VALUES
&& !DATAPOINT_NAME_SUBMIT.equals(datapoint.getName())
&& !DATAPOINT_NAME_INSTALL_TEST.equals(datapoint.getName())) {
symbols.add(datapoint.getName());
}
}
return symbols.toArray(new String[0]);
}
/**
* Returns the parsed text.
*/
public String getText() {
return text;
}
/**
* Returns the parsed beep value.
*/
public int getBeep() {
return beep;
}
/**
* Returns the parsed backlight value.
*/
public int getBacklight() {
return backlight;
}
/**
* Returns the parsed unit value.
*/
public int getUnit() {
return unit;
}
/**
* Returns the parsed symbols.
*/
public List<String> getSymbols() {
return symbols;
}
}

View File

@@ -0,0 +1,60 @@
/**
* 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.homematic.internal.communicator.parser;
import java.io.IOException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
/**
* Parses a event received from a Homematic gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public class EventParser extends CommonRpcParser<Object[], HmDatapointInfo> {
private Object value;
@Override
public HmDatapointInfo parse(Object[] message) throws IOException {
String address;
Integer channel = 0;
String addressWithChannel = toString(message[1]);
if ("".equals(addressWithChannel)) {
address = HmDevice.ADDRESS_GATEWAY_EXTRAS;
channel = HmChannel.CHANNEL_NUMBER_VARIABLE;
} else {
String[] configParts = StringUtils.trimToEmpty(addressWithChannel).split(":");
address = getSanitizedAddress(configParts[0]);
if (configParts.length > 1) {
channel = NumberUtils.createInteger(configParts[1]);
}
}
String name = toString(message[2]);
value = message[3];
return new HmDatapointInfo(address, HmParamsetType.VALUES, channel, name);
}
/**
* Returns the value of the event.
*/
public Object getValue() {
return value;
}
}

View File

@@ -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.homematic.internal.communicator.parser;
import java.io.IOException;
import org.apache.commons.lang.ObjectUtils;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* Parses a Homegear message with scripts and generates the scripts.
*
* @author Gerhard Riegler - Initial contribution
*/
public class GetAllScriptsParser extends CommonRpcParser<Object[], Void> {
private HmChannel channel;
public GetAllScriptsParser(HmChannel channel) {
this.channel = channel;
}
@Override
public Void parse(Object[] message) throws IOException {
message = (Object[]) message[0];
for (int i = 0; i < message.length; i++) {
String scriptName = ObjectUtils.toString(message[i]);
HmDatapoint dpScript = new HmDatapoint(scriptName, scriptName, HmValueType.BOOL, Boolean.FALSE, false,
HmParamsetType.VALUES);
dpScript.setInfo(scriptName);
channel.addDatapoint(dpScript);
}
return null;
}
}

View File

@@ -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.homematic.internal.communicator.parser;
import java.io.IOException;
import java.util.Map;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* Parses a Homegear message with variables and generates the datapoints.
*
* @author Gerhard Riegler - Initial contribution
*/
public class GetAllSystemVariablesParser extends CommonRpcParser<Object[], Void> {
private HmChannel channel;
public GetAllSystemVariablesParser(HmChannel channel) {
this.channel = channel;
}
@Override
@SuppressWarnings("unchecked")
public Void parse(Object[] message) throws IOException {
Map<String, ?> mapMessage = (Map<String, ?>) message[0];
for (String variableName : mapMessage.keySet()) {
Object value = mapMessage.get(variableName);
HmDatapoint dp = channel.getDatapoint(HmParamsetType.VALUES, variableName);
if (dp != null) {
dp.setValue(value);
} else {
HmDatapoint dpVariable = new HmDatapoint(variableName, variableName, guessType(value), value, false,
HmParamsetType.VALUES);
dpVariable.setInfo(variableName);
channel.addDatapoint(dpVariable);
}
}
return null;
}
/**
* Guesses the value type.
*/
private HmValueType guessType(Object value) {
if (value == null) {
return HmValueType.UNKNOWN;
} else if (value instanceof Boolean) {
return HmValueType.BOOL;
} else if (value instanceof Integer || value instanceof Long) {
return HmValueType.INTEGER;
} else if (value instanceof Number) {
return HmValueType.FLOAT;
} else {
return HmValueType.STRING;
}
}
}

View File

@@ -0,0 +1,60 @@
/**
* 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.homematic.internal.communicator.parser;
import java.io.IOException;
import java.util.Map;
/**
* Parses a getDeviceDescription message and extracts the type and firmware version.
*
* @author Gerhard Riegler - Initial contribution
*/
public class GetDeviceDescriptionParser extends CommonRpcParser<Object[], GetDeviceDescriptionParser> {
private String type;
private String firmware;
private String deviceInterface;
@SuppressWarnings("unchecked")
@Override
public GetDeviceDescriptionParser parse(Object[] message) throws IOException {
if (message != null && message.length > 0 && message[0] instanceof Map) {
Map<String, ?> mapMessage = (Map<String, ?>) message[0];
type = toString(mapMessage.get("TYPE"));
firmware = toString(mapMessage.get("FIRMWARE"));
deviceInterface = toString(mapMessage.get("INTERFACE"));
}
return this;
}
/**
* Returns the parsed type.
*/
public String getType() {
return type;
}
/**
* Returns the parsed firmware version.
*/
public String getFirmware() {
return firmware;
}
/**
* Returns the interface of the device.
*/
public String getDeviceInterface() {
return deviceInterface;
}
}

View File

@@ -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.homematic.internal.communicator.parser;
import java.io.IOException;
import java.util.Map;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmInterface;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Parses a parameter description message and extracts datapoint metadata.
*
* @author Gerhard Riegler - Initial contribution
*/
public class GetParamsetDescriptionParser extends CommonRpcParser<Object[], Void> {
private final Logger logger = LoggerFactory.getLogger(GetParamsetDescriptionParser.class);
private HmParamsetType paramsetType;
private HmChannel channel;
private boolean isHmIpDevice;
public GetParamsetDescriptionParser(HmChannel channel, HmParamsetType paramsetType) {
this.channel = channel;
this.paramsetType = paramsetType;
this.isHmIpDevice = channel.getDevice().getHmInterface() == HmInterface.HMIP;
}
@Override
@SuppressWarnings("unchecked")
public Void parse(Object[] message) throws IOException {
if (!(message[0] instanceof Map)) {
logger.debug("Unexpected datatype '{}', ignoring message", message[0].getClass());
return null;
}
Map<String, Map<String, Object>> dpNames = (Map<String, Map<String, Object>>) message[0];
for (String datapointName : dpNames.keySet()) {
Map<String, Object> dpMeta = dpNames.get(datapointName);
HmDatapoint dp = assembleDatapoint(datapointName, toString(dpMeta.get("UNIT")),
toString(dpMeta.get("TYPE")), toOptionList(dpMeta.get("VALUE_LIST")), dpMeta.get("MIN"),
dpMeta.get("MAX"), toInteger(dpMeta.get("OPERATIONS")), dpMeta.get("DEFAULT"), paramsetType,
isHmIpDevice);
channel.addDatapoint(dp);
}
return null;
}
}

View File

@@ -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.homematic.internal.communicator.parser;
import java.io.IOException;
import java.util.Map;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.binding.homematic.internal.model.HmInterface;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Parses a paramset message and extracts datapoint values.
*
* @author Gerhard Riegler - Initial contribution
*/
public class GetParamsetParser extends CommonRpcParser<Object[], Void> {
private final Logger logger = LoggerFactory.getLogger(GetParamsetParser.class);
private HmChannel channel;
private HmParamsetType paramsetType;
public GetParamsetParser(HmChannel channel, HmParamsetType paramsetType) {
this.channel = channel;
this.paramsetType = paramsetType;
}
@Override
@SuppressWarnings("unchecked")
public Void parse(Object[] message) throws IOException {
if (message == null || message.length == 0 || !(message[0] instanceof Map)) {
return null;
}
Map<String, ?> mapMessage = (Map<String, ?>) message[0];
for (String dpName : mapMessage.keySet()) {
HmDatapointInfo dpInfo = new HmDatapointInfo(paramsetType, channel, dpName);
HmDatapoint dp = channel.getDatapoint(dpInfo);
if (dp != null) {
dp.setValue(convertToType(dp, mapMessage.get(dpName)));
adjustRssiValue(dp);
} else {
// should never happen, but in case ...
// suppress warning for this datapoint due wrong CCU metadata
String deviceType = channel.getDevice().getType();
boolean isHmSenMdirNextTrans = dpInfo.getName().equals("NEXT_TRANSMISSION")
&& (deviceType.startsWith("HM-Sen-MDIR-O") || deviceType.startsWith("HM-Sen-MDIR-WM55")
|| deviceType.startsWith("HM-Sec-MDIR-2"));
if (!isHmSenMdirNextTrans) {
if (dpInfo.getParamsetType() == HmParamsetType.MASTER
&& channel.getDevice().getHmInterface() == HmInterface.HMIP) {
// These data points can't currently be recognized and therefore can't be created
logger.debug("Can't set value for channel configuration datapoint '{}'", dpInfo);
} else {
logger.warn("Can't set value for datapoint '{}'", dpInfo);
}
}
}
}
return null;
}
}

View File

@@ -0,0 +1,40 @@
/**
* 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.homematic.internal.communicator.parser;
import java.io.IOException;
import java.util.Map;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
/**
* Parses a getValue message from a Homematic gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public class GetValueParser extends CommonRpcParser<Object[], Void> {
private HmDatapoint dp;
public GetValueParser(HmDatapoint dp) {
this.dp = dp;
}
@Override
public Void parse(Object[] message) throws IOException {
if (message != null && message.length > 0 && !(message[0] instanceof Map)) {
dp.setValue(convertToType(dp, message[0]));
adjustRssiValue(dp);
}
return null;
}
}

View File

@@ -0,0 +1,57 @@
/**
* 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.homematic.internal.communicator.parser;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.openhab.binding.homematic.internal.model.HmDevice;
/**
* Parses a Homegear message containing names for devices.
*
* @author Gerhard Riegler - Initial contribution
*/
public class HomegearLoadDeviceNamesParser extends CommonRpcParser<Object[], Void> {
private Collection<HmDevice> devices;
public HomegearLoadDeviceNamesParser(Collection<HmDevice> devices) {
this.devices = devices;
}
@Override
@SuppressWarnings("unchecked")
public Void parse(Object[] message) throws IOException {
Map<String, HmDevice> devicesById = new HashMap<>();
for (HmDevice device : devices) {
devicesById.put(device.getHomegearId(), device);
}
message = (Object[]) message[0];
for (int i = 0; i < message.length; i++) {
Map<String, ?> data = (Map<String, ?>) message[i];
String id = toString(data.get("ID"));
String name = toString(data.get("NAME"));
HmDevice device = devicesById.get(getSanitizedAddress(id));
if (device != null) {
device.setName(name);
}
}
return null;
}
}

View File

@@ -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.homematic.internal.communicator.parser;
import java.io.IOException;
import java.util.Map;
/**
* Parses a listBidcosInterfaces message and extracts the type and gateway address.
*
* @author Gerhard Riegler - Initial contribution
*/
public class ListBidcosInterfacesParser extends CommonRpcParser<Object[], ListBidcosInterfacesParser> {
private String type;
private String gatewayAddress;
private String firmware;
private Integer dutyCycleRatio;
@SuppressWarnings("unchecked")
@Override
public ListBidcosInterfacesParser parse(Object[] message) throws IOException {
if (message != null && message.length > 0) {
message = (Object[]) message[0];
for (int i = 0; i < message.length; i++) {
Map<String, ?> mapMessage = (Map<String, ?>) message[i];
boolean isDefault = toBoolean(mapMessage.get("DEFAULT"));
if (isDefault) {
type = toString(mapMessage.get("TYPE"));
firmware = toString(mapMessage.get("FIRMWARE_VERSION"));
gatewayAddress = getSanitizedAddress(mapMessage.get("ADDRESS"));
dutyCycleRatio = toInteger(mapMessage.get("DUTY_CYCLE"));
}
}
}
return this;
}
/**
* Returns the parsed type.
*/
public String getType() {
return type;
}
/**
* Returns the parsed gateway address.
*/
public String getGatewayAddress() {
return gatewayAddress;
}
/**
* Returns the firmware version.
*/
public String getFirmware() {
return firmware;
}
/**
* Returns the duty cycle.
*/
public Integer getDutyCycleRatio() {
return dutyCycleRatio;
}
}

View File

@@ -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.homematic.internal.communicator.parser;
import static org.openhab.binding.homematic.internal.HomematicBindingConstants.CONFIGURATION_CHANNEL_NUMBER;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.misc.MiscUtils;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmInterface;
/**
* Parses a list devices message and generates device and channel metadata.
*
* @author Gerhard Riegler - Initial contribution
*/
public class ListDevicesParser extends CommonRpcParser<Object[], Collection<HmDevice>> {
private HmInterface hmInterface;
private HomematicConfig config;
public ListDevicesParser(HmInterface hmInterface, HomematicConfig config) {
this.hmInterface = hmInterface;
this.config = config;
}
@Override
@SuppressWarnings("unchecked")
public Collection<HmDevice> parse(Object[] message) throws IOException {
message = (Object[]) message[0];
Map<String, HmDevice> devices = new HashMap<>();
for (int i = 0; i < message.length; i++) {
Map<String, ?> data = (Map<String, ?>) message[i];
boolean isDevice = !StringUtils.contains(toString(data.get("ADDRESS")), ":");
if (isDevice) {
String address = getSanitizedAddress(data.get("ADDRESS"));
String type = MiscUtils.validateCharacters(toString(data.get("TYPE")), "Device type", "-");
String id = toString(data.get("ID"));
String firmware = toString(data.get("FIRMWARE"));
HmDevice device = new HmDevice(address, hmInterface, type, config.getGatewayInfo().getId(), id,
firmware);
device.addChannel(new HmChannel(type, CONFIGURATION_CHANNEL_NUMBER));
devices.put(address, device);
} else {
// channel
String deviceAddress = getSanitizedAddress(data.get("PARENT"));
HmDevice device = devices.get(deviceAddress);
String type = toString(data.get("TYPE"));
Integer number = toInteger(data.get("INDEX"));
device.addChannel(new HmChannel(type, number));
}
}
return devices.values();
}
}

View File

@@ -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.homematic.internal.communicator.parser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
/**
* Parses a new device event received from a Homematic gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public class NewDevicesParser extends CommonRpcParser<Object[], List<String>> {
@Override
@SuppressWarnings("unchecked")
public List<String> parse(Object[] message) throws IOException {
List<String> adresses = new ArrayList<>();
if (message != null && message.length > 1) {
message = (Object[]) message[1];
for (int i = 0; i < message.length; i++) {
Map<String, ?> data = (Map<String, ?>) message[i];
String address = toString(data.get("ADDRESS"));
boolean isDevice = !StringUtils.contains(address, ":")
&& !StringUtils.startsWithIgnoreCase(address, "BidCos");
if (isDevice) {
adresses.add(getSanitizedAddress(address));
}
}
}
return adresses;
}
}

View File

@@ -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.homematic.internal.communicator.parser;
import java.io.IOException;
/**
* Interface for all message parsers.
*
* @author Gerhard Riegler - Initial contribution
*/
public interface RpcParser<M, R> {
/**
* Parses the message returns the result.
*/
public R parse(M message) throws IOException;
}

View File

@@ -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.homematic.internal.communicator.parser;
import java.io.IOException;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.homematic.internal.communicator.client.UnknownParameterSetException;
import org.openhab.binding.homematic.internal.communicator.client.UnknownRpcFailureException;
import org.openhab.binding.homematic.internal.communicator.message.RpcRequest;
/**
* Parses the response from a RPC call, throws exception if fault response.
*
* @author Gerhard Riegler - Initial contribution
*/
public class RpcResponseParser extends CommonRpcParser<Object[], Object[]> {
private RpcRequest<?> request;
public RpcResponseParser(RpcRequest<?> request) {
this.request = request;
}
@Override
@SuppressWarnings("unchecked")
public Object[] parse(Object[] message) throws IOException {
if (message != null && message.length > 0) {
Object responseData = message[0];
if (responseData instanceof Map) {
Map<String, Object> map = (Map<String, Object>) responseData;
if (map.containsKey("faultCode")) {
Number faultCode = toNumber(map.get("faultCode"));
String faultString = toString(map.get("faultString"));
String faultMessage = String.format("%s %s (sending %s)", faultCode, faultString, request);
if (faultCode.intValue() == -1 && StringUtils.equals("Failure", faultString)) {
throw new UnknownRpcFailureException(faultMessage);
} else if (faultCode.intValue() == -3 && StringUtils.equals("Unknown paramset", faultString)) {
throw new UnknownParameterSetException(faultMessage);
}
throw new IOException(faultMessage);
}
}
return message;
}
throw new IOException("Unknown Result: " + message);
}
}

View File

@@ -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.homematic.internal.communicator.parser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.model.HmRssiInfo;
/**
* Parses a result with all rssi values of all datapoints.
*
* @author Gerhard Riegler - Initial contribution
*/
public class RssiInfoParser extends CommonRpcParser<Object[], List<HmRssiInfo>> {
private HomematicConfig config;
public RssiInfoParser(HomematicConfig config) {
this.config = config;
}
@Override
@SuppressWarnings("unchecked")
public List<HmRssiInfo> parse(Object[] result) throws IOException {
List<HmRssiInfo> rssiList = new ArrayList<>();
if (result != null && result.length > 0 && result[0] instanceof Map) {
Map<String, ?> devices = (Map<String, ?>) result[0];
for (String sourceDevice : devices.keySet()) {
Map<String, Object[]> targetDevices = (Map<String, Object[]>) devices.get(sourceDevice);
if (targetDevices != null) {
for (String targetDevice : targetDevices.keySet()) {
if (targetDevice.equals(config.getGatewayInfo().getAddress())) {
Integer rssiDevice = getAdjustedRssiValue((Integer) targetDevices.get(targetDevice)[0]);
Integer rssiPeer = getAdjustedRssiValue((Integer) targetDevices.get(targetDevice)[1]);
HmRssiInfo rssiInfo = new HmRssiInfo(sourceDevice, rssiDevice, rssiPeer);
rssiList.add(rssiInfo);
}
}
}
}
}
return rssiList;
}
}

View File

@@ -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.homematic.internal.communicator.server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.message.BinRpcMessage;
import org.openhab.binding.homematic.internal.communicator.message.RpcRequest;
import org.openhab.core.common.ThreadPoolManager;
/**
* Waits for a message from the Homematic gateway and starts the RpcCallbackHandler to handle the message.
*
* @author Gerhard Riegler - Initial contribution
*/
public class BinRpcNetworkService implements Runnable {
private static final byte BIN_EMPTY_STRING[] = { 'B', 'i', 'n', 1, 0, 0, 0, 8, 0, 0, 0, 3, 0, 0, 0, 0 };
private static final byte BIN_EMPTY_ARRAY[] = { 'B', 'i', 'n', 1, 0, 0, 0, 8, 0, 0, 1, 0, 0, 0, 0, 0 };
private static final byte BIN_EMPTY_EVENT_LIST[] = { 'B', 'i', 'n', 1, 0, 0, 0, 21, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0,
3, 0, 0, 0, 5, 'e', 'v', 'e', 'n', 't' };
private static final String RPC_POOL_NAME = "homematicRpc";
private ServerSocket serverSocket;
private boolean accept = true;
private HomematicConfig config;
private RpcResponseHandler<byte[]> rpcResponseHandler;
/**
* Creates the socket for listening to events from the Homematic gateway.
*/
public BinRpcNetworkService(RpcEventListener listener, HomematicConfig config) throws IOException {
this.config = config;
serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);
serverSocket.bind(new InetSocketAddress(config.getBindAddress(), config.getBinCallbackPort()));
this.rpcResponseHandler = new RpcResponseHandler<byte[]>(listener) {
@Override
protected byte[] getEmptyStringResult() {
return BIN_EMPTY_STRING;
}
@Override
protected byte[] getEmptyEventListResult() {
return BIN_EMPTY_EVENT_LIST;
}
@Override
protected byte[] getEmptyArrayResult() {
return BIN_EMPTY_ARRAY;
}
@Override
protected RpcRequest<byte[]> createRpcRequest() {
return new BinRpcMessage(null, BinRpcMessage.TYPE.RESPONSE, config.getEncoding());
}
};
}
/**
* Listening for events and starts the callbackHandler if a event received.
*/
@Override
public void run() {
while (accept) {
try {
Socket cs = serverSocket.accept();
BinRpcResponseHandler rpcHandler = new BinRpcResponseHandler(cs, rpcResponseHandler, config);
ThreadPoolManager.getPool(RPC_POOL_NAME).execute(rpcHandler);
} catch (IOException ex) {
// ignore
}
}
}
/**
* Stops the listening.
*/
public void shutdown() {
accept = false;
try {
serverSocket.close();
} catch (IOException ioe) {
// ignore
}
}
}

View File

@@ -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.homematic.internal.communicator.server;
import java.io.EOFException;
import java.io.IOException;
import java.net.Socket;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.message.BinRpcMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Reads a BIN-RPC message from the socket and handles the method call.
*
* @author Gerhard Riegler - Initial contribution
*/
public class BinRpcResponseHandler implements Runnable {
private final Logger logger = LoggerFactory.getLogger(BinRpcResponseHandler.class);
private Socket socket;
private RpcResponseHandler<byte[]> rpcResponseHandler;
private HomematicConfig config;
private long created;
public BinRpcResponseHandler(Socket socket, RpcResponseHandler<byte[]> rpcResponseHandler, HomematicConfig config) {
this.socket = socket;
this.rpcResponseHandler = rpcResponseHandler;
this.config = config;
this.created = System.currentTimeMillis();
}
/**
* Reads the event from the Homematic gateway and handles the method call.
*/
@Override
public void run() {
try {
boolean isMaxAliveReached;
do {
BinRpcMessage message = new BinRpcMessage(socket.getInputStream(), true, config.getEncoding());
logger.trace("Event BinRpcMessage: {}", message);
byte[] returnValue = rpcResponseHandler.handleMethodCall(message.getMethodName(),
message.getResponseData());
if (returnValue != null) {
socket.getOutputStream().write(returnValue);
}
isMaxAliveReached = System.currentTimeMillis() - created > (config.getSocketMaxAlive() * 1000);
} while (!isMaxAliveReached);
} catch (EOFException eof) {
// ignore
} catch (Exception e) {
logger.warn("{}", e.getMessage(), e);
} finally {
try {
socket.close();
} catch (IOException ioe) {
// ignore
}
}
}
}

View File

@@ -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.homematic.internal.communicator.server;
import java.io.IOException;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Server implementation for receiving messages via BIN-RPC from a Homematic gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public class BinRpcServer implements RpcServer {
private final Logger logger = LoggerFactory.getLogger(BinRpcServer.class);
private Thread networkServiceThread;
private BinRpcNetworkService networkService;
private HomematicConfig config;
private RpcEventListener listener;
public BinRpcServer(RpcEventListener listener, HomematicConfig config) {
this.listener = listener;
this.config = config;
}
@Override
public void start() throws IOException {
logger.debug("Initializing BIN-RPC server at port {}", config.getBinCallbackPort());
networkService = new BinRpcNetworkService(listener, config);
networkServiceThread = new Thread(networkService);
networkServiceThread.setName("HomematicRpcServer");
networkServiceThread.start();
}
@Override
public void shutdown() {
if (networkService != null) {
logger.debug("Stopping BIN-RPC server");
try {
if (networkServiceThread != null) {
networkServiceThread.interrupt();
}
} catch (Exception e) {
logger.error("{}", e.getMessage(), e);
}
networkService.shutdown();
networkService = null;
}
}
}

View File

@@ -0,0 +1,40 @@
/**
* 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.homematic.internal.communicator.server;
import java.util.List;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
/**
* Methods called by the RpcServer when a event is received.
*
* @author Gerhard Riegler - Initial contribution
*/
public interface RpcEventListener {
/**
* Called when a new event is received from a Homeamtic gateway.
*/
public void eventReceived(HmDatapointInfo dpInfo, Object newValue);
/**
* Called when new devices has been detected on the Homeamtic gateway.
*/
public void newDevices(List<String> adresses);
/**
* Called when devices has been deleted from the Homeamtic gateway.
*/
public void deleteDevices(List<String> addresses);
}

View File

@@ -0,0 +1,141 @@
/**
* 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.homematic.internal.communicator.server;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.ObjectUtils;
import org.openhab.binding.homematic.internal.communicator.message.RpcRequest;
import org.openhab.binding.homematic.internal.communicator.parser.DeleteDevicesParser;
import org.openhab.binding.homematic.internal.communicator.parser.EventParser;
import org.openhab.binding.homematic.internal.communicator.parser.NewDevicesParser;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Common RPC response methods.
*
* @author Gerhard Riegler - Initial contribution
*/
public abstract class RpcResponseHandler<T> {
private final Logger logger = LoggerFactory.getLogger(RpcResponseHandler.class);
private RpcEventListener listener;
public RpcResponseHandler(RpcEventListener listener) {
this.listener = listener;
}
/**
* Returns a valid result of the method called by the Homematic gateway.
*/
public T handleMethodCall(String methodName, Object[] responseData) throws IOException {
if (RPC_METHODNAME_EVENT.equals(methodName)) {
return handleEvent(responseData);
} else if (RPC_METHODNAME_LIST_DEVICES.equals(methodName) || RPC_METHODNAME_UPDATE_DEVICE.equals(methodName)) {
return getEmptyArrayResult();
} else if (RPC_METHODNAME_DELETE_DEVICES.equals(methodName)) {
return handleDeleteDevice(responseData);
} else if (RPC_METHODNAME_NEW_DEVICES.equals(methodName)) {
return handleNewDevice(responseData);
} else if (RPC_METHODNAME_SYSTEM_LISTMETHODS.equals(methodName)) {
RpcRequest<T> msg = createRpcRequest();
msg.addArg(getListMethods());
return msg.createMessage();
} else if (RPC_METHODNAME_SYSTEM_MULTICALL.equals(methodName)) {
for (Object o : (Object[]) responseData[0]) {
Map<?, ?> call = (Map<?, ?>) o;
if (call != null) {
String method = ObjectUtils.toString(call.get("methodName"));
Object[] data = (Object[]) call.get("params");
handleMethodCall(method, data);
}
}
return getEmptyEventListResult();
} else if (RPC_METHODNAME_SET_CONFIG_READY.equals(methodName)) {
return getEmptyEventListResult();
} else {
logger.warn("Unknown method called by Homematic gateway: {}", methodName);
return getEmptyEventListResult();
}
}
/**
* Creates a BINRPC message with the supported method names.
*/
private List<String> getListMethods() {
List<String> events = new ArrayList<>();
events.add(RPC_METHODNAME_SYSTEM_MULTICALL);
events.add(RPC_METHODNAME_EVENT);
events.add(RPC_METHODNAME_DELETE_DEVICES);
events.add(RPC_METHODNAME_NEW_DEVICES);
return events;
}
/**
* Populates the extracted event to the listener.
*/
private T handleEvent(Object[] message) throws IOException {
EventParser eventParser = new EventParser();
HmDatapointInfo dpInfo = eventParser.parse(message);
listener.eventReceived(dpInfo, eventParser.getValue());
return getEmptyStringResult();
}
/**
* Calls the listener when a devices has been detected.
*/
private T handleNewDevice(Object[] message) throws IOException {
NewDevicesParser ndParser = new NewDevicesParser();
List<String> adresses = ndParser.parse(message);
listener.newDevices(adresses);
return getEmptyArrayResult();
}
/**
* Calls the listener when devices has been deleted.
*/
private T handleDeleteDevice(Object[] message) throws IOException {
DeleteDevicesParser ddParser = new DeleteDevicesParser();
List<String> adresses = ddParser.parse(message);
listener.deleteDevices(adresses);
return getEmptyArrayResult();
}
/**
* Returns a predefined result for an empty string.
*/
protected abstract T getEmptyStringResult();
/**
* Returns a predefined result for an empty array.
*/
protected abstract T getEmptyArrayResult();
/**
* Returns a predefined result for an empty event list.
*/
protected abstract T getEmptyEventListResult();
/**
* Creates a typed RpcRequest.
*/
protected abstract RpcRequest<T> createRpcRequest();
}

View File

@@ -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.homematic.internal.communicator.server;
import java.io.IOException;
/**
* Simple RPC server interface.
*
* @author Gerhard Riegler - Initial contribution
*/
public interface RpcServer {
/**
* Starts the rpc server.
*/
public void start() throws IOException;
/**
* Stops the rpc server.
*/
public void shutdown();
}

View File

@@ -0,0 +1,138 @@
/**
* 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.homematic.internal.communicator.server;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.message.RpcRequest;
import org.openhab.binding.homematic.internal.communicator.message.XmlRpcRequest;
import org.openhab.binding.homematic.internal.communicator.message.XmlRpcResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
/**
* Reads a XML-RPC message and handles the method call.
*
* @author Gerhard Riegler - Initial contribution
*/
public class XmlRpcServer implements RpcServer {
private final Logger logger = LoggerFactory.getLogger(XmlRpcServer.class);
private static final String XML_EMPTY_STRING = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n<methodResponse><params><param><value></value></param></params></methodResponse>";
private static final String XML_EMPTY_ARRAY = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n<methodResponse><params><param><value><array><data></data></array></value></param></params></methodResponse>";
private static final String XML_EMPTY_EVENT_LIST = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n<methodResponse><params><param><value><array><data><value>event</value></data></array></value></param></params></methodResponse>";
private Server xmlRpcHTTPD;
private HomematicConfig config;
private RpcResponseHandler<String> rpcResponseHander;
private final ResponseHandler jettyResponseHandler = new ResponseHandler();
public XmlRpcServer(RpcEventListener listener, HomematicConfig config) {
this.config = config;
this.rpcResponseHander = new RpcResponseHandler<String>(listener) {
@Override
protected String getEmptyStringResult() {
return XML_EMPTY_STRING;
}
@Override
protected String getEmptyEventListResult() {
return XML_EMPTY_EVENT_LIST;
}
@Override
protected String getEmptyArrayResult() {
return XML_EMPTY_ARRAY;
}
@Override
protected RpcRequest<String> createRpcRequest() {
return new XmlRpcRequest(null, XmlRpcRequest.TYPE.RESPONSE);
}
};
}
@Override
public void start() throws IOException {
logger.debug("Initializing XML-RPC server at port {}", config.getXmlCallbackPort());
InetSocketAddress callbackAddress = new InetSocketAddress(config.getBindAddress(), config.getXmlCallbackPort());
xmlRpcHTTPD = new Server(callbackAddress);
xmlRpcHTTPD.setHandler(jettyResponseHandler);
try {
xmlRpcHTTPD.start();
if (logger.isTraceEnabled()) {
xmlRpcHTTPD.dumpStdErr();
}
} catch (Exception e) {
throw new IOException("Jetty start failed", e);
}
}
@Override
public void shutdown() {
if (xmlRpcHTTPD != null) {
logger.debug("Stopping XML-RPC server");
try {
xmlRpcHTTPD.stop();
} catch (Exception ex) {
logger.error("{}", ex.getMessage(), ex);
}
}
}
/**
* Response handler for Jetty implementing a XML-RPC server
*
* @author Martin Herbst
*/
private class ResponseHandler extends AbstractHandler {
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
response.setContentType("text/xml;charset=ISO-8859-1");
response.setStatus(HttpServletResponse.SC_OK);
final PrintWriter respWriter = response.getWriter();
try {
XmlRpcResponse xmlResponse = new XmlRpcResponse(request.getInputStream(), config.getEncoding());
if (logger.isTraceEnabled()) {
logger.trace("Server parsed XmlRpcMessage:\n{}", xmlResponse);
}
final String returnValue = rpcResponseHander.handleMethodCall(xmlResponse.getMethodName(),
xmlResponse.getResponseData());
if (logger.isTraceEnabled()) {
logger.trace("Server XmlRpcResponse:\n{}", returnValue);
}
respWriter.println(returnValue);
} catch (SAXException | ParserConfigurationException ex) {
logger.error("{}", ex.getMessage(), ex);
respWriter.println(XML_EMPTY_STRING);
}
baseRequest.setHandled(true);
}
}
}

View File

@@ -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.homematic.internal.communicator.virtual;
import java.io.IOException;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.openhab.binding.homematic.internal.model.HmValueType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract base class for all virtual datapoints with common methods.
*
* @author Gerhard Riegler - Initial contribution
*/
public abstract class AbstractVirtualDatapointHandler implements VirtualDatapointHandler {
private final Logger logger = LoggerFactory.getLogger(AbstractVirtualDatapointHandler.class);
@Override
public boolean canHandleCommand(HmDatapoint dp, Object value) {
return false;
}
@Override
public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointConfig dpConfig, Object value)
throws IOException, HomematicClientException {
}
@Override
public boolean canHandleEvent(HmDatapoint dp) {
return false;
}
@Override
public void handleEvent(VirtualGateway gateway, HmDatapoint dp) {
}
@Override
public HmDatapoint getVirtualDatapoint(HmChannel channel) {
return channel.getDatapoint(HmParamsetType.VALUES, getName());
}
/**
* Creates a new datapoint with the given parameters and adds it to the channel.
*/
protected HmDatapoint addDatapoint(HmDevice device, Integer channelNumber, String datapointName,
HmValueType valueType, Object value, boolean readOnly) {
HmChannel channel = device.getChannel(channelNumber);
HmDatapoint dp = new HmDatapoint(datapointName, datapointName, valueType, value, readOnly,
HmParamsetType.VALUES);
return addDatapoint(channel, dp);
}
/**
* Adds a new datapoint to the channel.
*/
protected HmDatapoint addDatapoint(HmChannel channel, HmDatapoint dp) {
logger.trace("Adding virtual datapoint '{}' to device '{}' ({}) and channel {}", dp.getName(),
channel.getDevice().getAddress(), channel.getDevice().getType(), channel.getNumber());
dp.setVirtual(true);
dp.setReadable(true);
channel.addDatapoint(dp);
return dp;
}
}

View File

@@ -0,0 +1,60 @@
/**
* 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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.VIRTUAL_DATAPOINT_NAME_BATTERY_TYPE;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmValueType;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A virtual String datapoint which adds the battery type to a battery powered device.
*
* @author Gerhard Riegler - Initial contribution
*/
public class BatteryTypeVirtualDatapointHandler extends AbstractVirtualDatapointHandler {
private final Logger logger = LoggerFactory.getLogger(BatteryTypeVirtualDatapointHandler.class);
private static final Properties batteries = new Properties();
public BatteryTypeVirtualDatapointHandler() {
Bundle bundle = FrameworkUtil.getBundle(getClass());
try (InputStream stream = bundle.getResource("homematic/batteries.properties").openStream()) {
batteries.load(stream);
} catch (IllegalStateException | IOException e) {
logger.warn("The resource homematic/batteries.properties could not be loaded! Battery types not available",
e);
}
}
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_BATTERY_TYPE;
}
@Override
public void initialize(HmDevice device) {
String batteryType = batteries.getProperty(device.getType());
if (batteryType != null) {
addDatapoint(device, 0, getName(), HmValueType.STRING, batteryType, true);
}
}
}

View File

@@ -0,0 +1,88 @@
/**
* 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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.VIRTUAL_DATAPOINT_NAME_BUTTON;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.homematic.internal.misc.MiscUtils;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmValueType;
import org.openhab.core.thing.CommonTriggerEvents;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A virtual String datapoint which adds a BUTTON datapoint. It will forward key events to the
* system channel {@link DefaultSystemChannelTypeProvider#SYSTEM_BUTTON}.
*
* @author Michael Reitler - Initial contribution
*/
public class ButtonVirtualDatapointHandler extends AbstractVirtualDatapointHandler {
private final Logger logger = LoggerFactory.getLogger(ButtonVirtualDatapointHandler.class);
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_BUTTON;
}
@Override
public void initialize(HmDevice device) {
for (HmChannel channel : device.getChannels()) {
if (channel.hasPressDatapoint()) {
HmDatapoint dp = addDatapoint(device, channel.getNumber(), getName(), HmValueType.STRING, null, false);
dp.setTrigger(true);
dp.setOptions(new String[] { CommonTriggerEvents.SHORT_PRESSED, CommonTriggerEvents.LONG_PRESSED,
CommonTriggerEvents.DOUBLE_PRESSED });
}
}
}
@Override
public boolean canHandleEvent(HmDatapoint dp) {
return dp.isPressDatapoint();
}
@Override
public void handleEvent(VirtualGateway gateway, HmDatapoint dp) {
HmDatapoint vdp = getVirtualDatapoint(dp.getChannel());
if (MiscUtils.isTrueValue(dp.getValue())) {
String pressType = StringUtils.substringAfter(dp.getName(), "_");
switch (pressType) {
case "SHORT":
if (dp.getValue() == null || !dp.getValue().equals(dp.getPreviousValue())) {
vdp.setValue(CommonTriggerEvents.SHORT_PRESSED);
} else {
// two (or more) PRESS_SHORT events were received
// within AbstractHomematicGateway#DEFAULT_DISABLE_DELAY seconds
vdp.setValue(CommonTriggerEvents.DOUBLE_PRESSED);
}
break;
case "LONG":
vdp.setValue(CommonTriggerEvents.LONG_PRESSED);
break;
case "LONG_RELEASE":
case "CONT":
vdp.setValue(null);
break;
default:
vdp.setValue(null);
logger.warn("Unexpected vaule '{}' for PRESS virtual datapoint", pressType);
}
} else {
vdp.setValue(null);
}
}
}

View File

@@ -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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.VIRTUAL_DATAPOINT_NAME_DELETE_DEVICE_MODE;
import java.io.IOException;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmInterface;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* A virtual enum datapoint which holds the delete flag for deleting a device with the DELETE_DEVICE virtual datapoint.
* Falls back to LOCKED after 30 seconds to prevent accidental deletion.
*
* @author Gerhard Riegler - Initial contribution
*/
public class DeleteDeviceModeVirtualDatapointHandler extends AbstractVirtualDatapointHandler {
protected static final String MODE_LOCKED = "LOCKED";
protected static final String MODE_RESET = "RESET";
protected static final String MODE_FORCE = "FORCE";
protected static final String MODE_DEFER = "DEFER";
private static final int DELETE_MODE_DURATION = 30;
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_DELETE_DEVICE_MODE;
}
@Override
public void initialize(HmDevice device) {
if (!device.isGatewayExtras() && !(device.getHmInterface() == HmInterface.CUXD)) {
HmDatapoint dp = addDatapoint(device, 0, getName(), HmValueType.ENUM, 0, false);
dp.setOptions(new String[] { MODE_LOCKED, MODE_RESET, MODE_FORCE, MODE_DEFER });
dp.setMinValue(0);
dp.setMaxValue(dp.getOptions().length - 1);
}
}
@Override
public boolean canHandleCommand(HmDatapoint dp, Object value) {
return getName().equals(dp.getName());
}
@Override
public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointConfig dpConfig, Object value)
throws IOException, HomematicClientException {
dp.setValue(value);
if (!StringUtils.equals(dp.getOptionValue(), MODE_LOCKED)) {
gateway.disableDatapoint(dp, DELETE_MODE_DURATION);
}
}
}

View File

@@ -0,0 +1,88 @@
/**
* 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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.communicator.virtual.DeleteDeviceModeVirtualDatapointHandler.*;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
import java.io.IOException;
import org.openhab.binding.homematic.internal.communicator.AbstractHomematicGateway;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.misc.MiscUtils;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmInterface;
import org.openhab.binding.homematic.internal.model.HmValueType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A virtual boolean datapoint which locks the device so it can not be accidentally removed from the gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public class DeleteDeviceVirtualDatapointHandler extends AbstractVirtualDatapointHandler {
private final Logger logger = LoggerFactory.getLogger(DeleteDeviceVirtualDatapointHandler.class);
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_DELETE_DEVICE;
}
@Override
public void initialize(HmDevice device) {
if (!device.isGatewayExtras() && !(device.getHmInterface() == HmInterface.CUXD)) {
addDatapoint(device, 0, getName(), HmValueType.BOOL, Boolean.FALSE, false);
}
}
@Override
public boolean canHandleCommand(HmDatapoint dp, Object value) {
return getName().equals(dp.getName());
}
@Override
public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointConfig dpConfig, Object value)
throws IOException, HomematicClientException {
dp.setValue(value);
if (MiscUtils.isTrueValue(dp.getValue())) {
try {
HmDatapoint deleteMode = dp.getChannel().getDatapoint(
HmDatapointInfo.createValuesInfo(dp.getChannel(), VIRTUAL_DATAPOINT_NAME_DELETE_DEVICE_MODE));
HmDevice device = dp.getChannel().getDevice();
int flag = -1;
switch (deleteMode.getOptionValue()) {
case MODE_RESET:
flag = 1;
break;
case MODE_FORCE:
flag = 2;
break;
case MODE_DEFER:
flag = 4;
}
if (flag == -1) {
logger.info("Can't delete device '{}' from gateway '{}', DELETE_MODE is LOCKED",
device.getAddress(), gateway.getId());
} else {
gateway.getRpcClient(device.getHmInterface()).deleteDevice(device, flag);
}
} finally {
gateway.disableDatapoint(dp, AbstractHomematicGateway.DEFAULT_DISABLE_DELAY);
}
}
}
}

View File

@@ -0,0 +1,85 @@
/**
* 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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
import java.io.IOException;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.homematic.internal.communicator.parser.DisplayOptionsParser;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmInterface;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* A virtual String datapoint to control the display of a 19 button remote control. You can send a text and/or show
* symbols, turn on the backlight and let the remote control beep.
*
* @author Gerhard Riegler - Initial contribution
*/
public class DisplayOptionsVirtualDatapointHandler extends AbstractVirtualDatapointHandler {
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_DISPLAY_OPTIONS;
}
@Override
public void initialize(HmDevice device) {
if (device.getType().startsWith(DEVICE_TYPE_19_REMOTE_CONTROL)
&& !(device.getHmInterface() == HmInterface.CUXD)) {
addDatapoint(device, 18, getName(), HmValueType.STRING, null, false);
}
}
@Override
public boolean canHandleCommand(HmDatapoint dp, Object value) {
return getName().equals(dp.getName());
}
@Override
public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointConfig dpConfig, Object value)
throws IOException, HomematicClientException {
HmChannel channel = dp.getChannel();
DisplayOptionsParser rcOptionsParser = new DisplayOptionsParser(channel);
rcOptionsParser.parse(value);
if (StringUtils.isNotBlank(rcOptionsParser.getText())) {
sendDatapoint(gateway, channel, DATAPOINT_NAME_TEXT, rcOptionsParser.getText());
}
sendDatapoint(gateway, channel, DATAPOINT_NAME_BEEP, rcOptionsParser.getBeep());
sendDatapoint(gateway, channel, DATAPOINT_NAME_UNIT, rcOptionsParser.getUnit());
sendDatapoint(gateway, channel, DATAPOINT_NAME_BACKLIGHT, rcOptionsParser.getBacklight());
for (String symbol : rcOptionsParser.getSymbols()) {
sendDatapoint(gateway, channel, symbol, Boolean.TRUE);
}
sendDatapoint(gateway, channel, DATAPOINT_NAME_SUBMIT, Boolean.TRUE);
dp.setValue(value);
}
private void sendDatapoint(VirtualGateway gateway, HmChannel channel, String dpName, Object newValue)
throws IOException, HomematicClientException {
HmDatapointInfo dpInfo = HmDatapointInfo.createValuesInfo(channel, dpName);
HmDatapoint dp = gateway.getDatapoint(dpInfo);
gateway.sendDatapoint(dp, new HmDatapointConfig(), newValue, null);
}
}

View File

@@ -0,0 +1,415 @@
/**
* 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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.misc.MiscUtils;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* The {@link DisplayTextVirtualDatapoint} adds multiple virtual datapoints to the HM-Dis-WM55 and HM-Dis-EP-WM55
* devices to easily handle colored text, icons, the led and the beeper of the display.
*
* @author Gerhard Riegler - Initial contribution
*/
public class DisplayTextVirtualDatapoint extends AbstractVirtualDatapointHandler {
private static final String DATAPOINT_NAME_DISPLAY_LINE = "DISPLAY_LINE_";
private static final String DATAPOINT_NAME_DISPLAY_COLOR = "DISPLAY_COLOR_";
private static final String DATAPOINT_NAME_DISPLAY_ICON = "DISPLAY_ICON_";
private static final String DATAPOINT_NAME_DISPLAY_LED = "DISPLAY_LED";
private static final String DATAPOINT_NAME_DISPLAY_BEEPER = "DISPLAY_BEEPER";
private static final String DATAPOINT_NAME_DISPLAY_BEEPCOUNT = "DISPLAY_BEEPCOUNT";
private static final String DATAPOINT_NAME_DISPLAY_BEEPINTERVAL = "DISPLAY_BEEPINTERVAL";
private static final String DATAPOINT_NAME_DISPLAY_SUBMIT = "DISPLAY_SUBMIT";
private static final String START = "0x02";
private static final String STOP = "0x03";
private static final String LINE = "0x12";
private static final String COLOR = "0x11";
private static final String LF = "0x0a";
private static final String ICON = "0x13";
private static final String BEEPER_START = "0x14";
private static final String BEEPER_END = "0x1c";
private static final String BEEPCOUNT_END = "0x1D";
private static final String BEEPINTERVAL_END = "0x16";
private static Map<String, String> replaceMap = new HashMap<>();
// replace special chars while encoding
static {
replaceMap.put("d6", "23");
replaceMap.put("dc", "24");
replaceMap.put("3d", "27");
replaceMap.put("c4", "5b");
replaceMap.put("df", "5f");
replaceMap.put("e4", "7b");
replaceMap.put("f6", "7c");
replaceMap.put("fc", "7d");
}
/**
* Available text colors.
*/
private enum Color {
NONE(""),
WHITE("0x80"),
RED("0x81"),
ORANGE("0x82"),
YELLOW("0x83"),
GREEN("0x84"),
BLUE("0x85");
private final String code;
private Color(String code) {
this.code = code;
}
protected String getCode() {
return code;
}
/**
* Returns the color code.
*/
public static String getCode(String name) {
try {
return valueOf(name).getCode();
} catch (Exception ex) {
return null;
}
}
}
/**
* Available icons.
*/
private enum Icon {
NONE(""),
OFF("0x80"),
ON("0x81"),
OPEN("0x82"),
CLOSED("0x83"),
ERROR("0x84"),
OK("0x85"),
INFO("0x86"),
NEW_MESSAGE("0x87"),
SERVICE("0x88"),
SIGNAL_GREEN("0x89"),
SIGNAL_YELLOW("0x8a"),
SIGNAL_RED("0x8b");
private final String code;
private Icon(String code) {
this.code = code;
}
protected String getCode() {
return code;
}
/**
* Returns the icon code.
*/
public static String getCode(String name) {
try {
return valueOf(name).getCode();
} catch (Exception ex) {
return null;
}
}
}
/**
* Available Beeper codes.
*/
private enum Beeper {
OFF("0xc0"),
LONG_LONG("0xc1"),
LONG_SHORT("0xc2"),
LONG_SHORT_SHORT("0xc3"),
SHORT("0xc4"),
SHORT_SHORT("0xc5"),
LONG("0xc6");
private final String code;
private Beeper(String code) {
this.code = code;
}
protected String getCode() {
return code;
}
/**
* Returns the beeper code.
*/
public static String getCode(String name) {
try {
return valueOf(name).getCode();
} catch (Exception ex) {
return OFF.getCode();
}
}
}
/**
* Available LED colors.
*/
private enum Led {
OFF("0xf0"),
RED("0xf1"),
GREEN("0xf2"),
ORANGE("0xf3");
private final String code;
private Led(String code) {
this.code = code;
}
protected String getCode() {
return code;
}
/**
* Returns the LED code.
*/
public static String getCode(String name) {
try {
return valueOf(name).getCode();
} catch (Exception ex) {
return OFF.getCode();
}
}
}
@Override
public String getName() {
return DATAPOINT_NAME_DISPLAY_SUBMIT;
}
@Override
public void initialize(HmDevice device) {
if (isDisplay(device)) {
for (HmChannel channel : device.getChannels()) {
if (channel.hasDatapoint(new HmDatapointInfo(HmParamsetType.VALUES, channel, DATAPOINT_NAME_SUBMIT))) {
for (int i = 1; i <= getLineCount(device); i++) {
addDatapoint(device, channel.getNumber(), DATAPOINT_NAME_DISPLAY_LINE + i, HmValueType.STRING,
null, false);
addEnumDisplayDatapoint(device, channel.getNumber(), DATAPOINT_NAME_DISPLAY_ICON + i,
Icon.class);
if (!isEpDisplay(device)) {
addEnumDisplayDatapoint(device, channel.getNumber(), DATAPOINT_NAME_DISPLAY_COLOR + i,
Color.class);
}
}
if (isEpDisplay(device)) {
addEnumDisplayDatapoint(device, channel.getNumber(), DATAPOINT_NAME_DISPLAY_BEEPER,
Beeper.class);
HmDatapoint bc = addDatapoint(device, channel.getNumber(), DATAPOINT_NAME_DISPLAY_BEEPCOUNT,
HmValueType.INTEGER, 1, false);
bc.setMinValue(0);
bc.setMaxValue(15);
HmDatapoint bd = addDatapoint(device, channel.getNumber(), DATAPOINT_NAME_DISPLAY_BEEPINTERVAL,
HmValueType.INTEGER, 1, false);
bd.setMinValue(10);
bd.setMaxValue(160);
bd.setStep(10);
addEnumDisplayDatapoint(device, channel.getNumber(), DATAPOINT_NAME_DISPLAY_LED, Led.class);
}
addDatapoint(device, channel.getNumber(), DATAPOINT_NAME_DISPLAY_SUBMIT, HmValueType.BOOL, false,
false);
}
}
}
}
/**
* Adds a Datapoint to the device with the values of the given enum.
*/
private void addEnumDisplayDatapoint(HmDevice device, int channelNumber, String datapointName,
Class<? extends Enum<?>> e) {
HmDatapoint dpEnum = addDatapoint(device, channelNumber, datapointName, HmValueType.ENUM, null, false);
dpEnum.setOptions(getEnumNames(e));
dpEnum.setMinValue(0);
dpEnum.setMaxValue(e.getEnumConstants().length);
}
/**
* Returns a string array with all the constants in the Enum.
*/
private String[] getEnumNames(Class<? extends Enum<?>> e) {
return Arrays.toString(e.getEnumConstants()).replaceAll("^.|.$", "").split(", ");
}
/**
* Returns the number of lines of the display.
*/
private int getLineCount(HmDevice device) {
return (DEVICE_TYPE_STATUS_DISPLAY.equals(device.getType()) ? 6 : 3);
}
/**
* Returns true, if the display is a EP display.
*/
private boolean isEpDisplay(HmDevice device) {
return DEVICE_TYPE_EP_STATUS_DISPLAY.equals(device.getType());
}
/**
* Returns true, if the device is a supported display.
*/
private boolean isDisplay(HmDevice device) {
return device.getType().equals(DEVICE_TYPE_STATUS_DISPLAY) || isEpDisplay(device);
}
@Override
public boolean canHandleCommand(HmDatapoint dp, Object value) {
HmDevice device = dp.getChannel().getDevice();
return (device.getType().equals(DEVICE_TYPE_STATUS_DISPLAY) || isEpDisplay(device))
&& (getName().equals(dp.getName()) || dp.getName().startsWith(DATAPOINT_NAME_DISPLAY_LINE)
|| dp.getName().startsWith(DATAPOINT_NAME_DISPLAY_COLOR)
|| dp.getName().startsWith(DATAPOINT_NAME_DISPLAY_ICON)
|| dp.getName().equals(DATAPOINT_NAME_DISPLAY_LED)
|| dp.getName().equals(DATAPOINT_NAME_DISPLAY_BEEPER)
|| dp.getName().equals(DATAPOINT_NAME_DISPLAY_BEEPCOUNT)
|| dp.getName().equals(DATAPOINT_NAME_DISPLAY_BEEPINTERVAL));
}
@Override
public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointConfig dpConfig, Object value)
throws IOException, HomematicClientException {
dp.setValue(value);
if (DATAPOINT_NAME_DISPLAY_SUBMIT.equals(dp.getName()) && MiscUtils.isTrueValue(dp.getValue())) {
HmChannel channel = dp.getChannel();
boolean isEp = isEpDisplay(channel.getDevice());
List<String> message = new ArrayList<>();
message.add(START);
if (isEp) {
message.add(LF);
}
for (int i = 1; i <= getLineCount(channel.getDevice()); i++) {
String line = ObjectUtils.toString(
channel.getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_DISPLAY_LINE + i).getValue());
if (StringUtils.isEmpty(line)) {
line = " ";
}
message.add(LINE);
message.add(encodeText(line));
if (!isEp) {
String color = channel.getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_DISPLAY_COLOR + i)
.getOptionValue();
message.add(COLOR);
String colorCode = Color.getCode(color);
message.add(StringUtils.isBlank(colorCode) ? Color.WHITE.getCode() : colorCode);
}
String icon = channel.getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_DISPLAY_ICON + i)
.getOptionValue();
String iconCode = Icon.getCode(icon);
if (StringUtils.isNotBlank(iconCode)) {
message.add(ICON);
message.add(iconCode);
}
message.add(LF);
}
if (isEp) {
String beeper = channel.getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_DISPLAY_BEEPER)
.getOptionValue();
message.add(BEEPER_START);
message.add(Beeper.getCode(beeper));
message.add(BEEPER_END);
// set number of beeps
message.add(
encodeBeepCount(channel.getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_DISPLAY_BEEPCOUNT)));
message.add(BEEPCOUNT_END);
// set interval between two beeps
message.add(encodeBeepInterval(
channel.getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_DISPLAY_BEEPINTERVAL)));
message.add(BEEPINTERVAL_END);
// LED value must always set (same as beeps)
String led = channel.getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_DISPLAY_LED).getOptionValue();
message.add(Led.getCode(led));
}
message.add(STOP);
gateway.sendDatapoint(channel.getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_SUBMIT),
new HmDatapointConfig(), StringUtils.join(message, ","), null);
}
}
/**
* Encodes the beep count value. Allowed values 0 - 15, where 0 means infinite.
*/
private String encodeBeepCount(HmDatapoint dp) {
int counts = (int) (Number) dp.getValue();
if (counts == 0) {
counts = 16;
}
return String.format("0x%02x", 207 + counts);
}
/**
* Encodes the beep interval value in 10 s steps. Allowed values 10 - 160.
*/
private String encodeBeepInterval(HmDatapoint dp) {
int interval = (int) (Number) dp.getValue();
return String.format("0x%02x", 224 + ((interval - 1) / 10));
}
/**
* Encodes the given text for the display.
*/
private String encodeText(String text) {
final byte[] bytes = text.getBytes(StandardCharsets.ISO_8859_1);
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append("0x");
String hexValue = String.format("%02x", b);
sb.append(replaceMap.containsKey(hexValue) ? replaceMap.get(hexValue) : hexValue);
sb.append(",");
}
sb.setLength(sb.length() - 1);
return sb.toString();
}
}

View File

@@ -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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.VIRTUAL_DATAPOINT_NAME_FIRMWARE;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* A virtual String datapoint which adds the firmware version to the device.
*
* @author Gerhard Riegler - Initial contribution
*/
public class FirmwareVirtualDatapointHandler extends AbstractVirtualDatapointHandler {
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_FIRMWARE;
}
@Override
public void initialize(HmDevice device) {
if (!device.isGatewayExtras()) {
addDatapoint(device, 0, getName(), HmValueType.STRING, device.getFirmware(), true);
}
}
}

View File

@@ -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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* Adds a STATE, VALUE and CALIBRATION datapoint to the HMW-IO-12-Sw14-DR device.
* This device can change its metadata depending on the configuration. This virtual datapoint ensures, that always all
* datapoints are available.
*
* @author Gerhard Riegler - Initial contribution
*/
public class HmwIoModuleVirtualDatapointHandler extends AbstractVirtualDatapointHandler {
@Override
public String getName() {
return null;
}
@Override
public void initialize(HmDevice device) {
if (device.getType().startsWith(DEVICE_TYPE_WIRED_IO_MODULE)) {
for (HmChannel channel : device.getChannels()) {
if (channel.getNumber() >= 7) {
HmDatapointInfo dpInfoState = HmDatapointInfo.createValuesInfo(channel, DATAPOINT_NAME_STATE);
HmDatapointInfo dpInfoValue = HmDatapointInfo.createValuesInfo(channel, DATAPOINT_NAME_VALUE);
boolean hasStateDatapoint = channel.hasDatapoint(dpInfoState);
boolean hasValueDatapoint = channel.hasDatapoint(dpInfoValue);
if (hasStateDatapoint && !hasValueDatapoint) {
HmDatapoint dp = addDatapoint(channel.getDevice(), channel.getNumber(), DATAPOINT_NAME_VALUE,
HmValueType.FLOAT, 0.0, false);
dp.setMinValue(0.0);
dp.setMaxValue(1000.0);
dp.setVirtual(false);
} else if (hasValueDatapoint && !hasStateDatapoint) {
HmDatapoint dp = addDatapoint(channel.getDevice(), channel.getNumber(), DATAPOINT_NAME_STATE,
HmValueType.BOOL, false, false);
dp.setVirtual(false);
}
}
if (channel.getNumber() >= 21) {
HmDatapointInfo dpInfoCalibration = new HmDatapointInfo(HmParamsetType.MASTER, channel,
DATAPOINT_NAME_CALIBRATION);
if (!channel.hasDatapoint(dpInfoCalibration)) {
HmDatapoint dp = new HmDatapoint(DATAPOINT_NAME_CALIBRATION, DATAPOINT_NAME_CALIBRATION,
HmValueType.INTEGER, 0, false, HmParamsetType.MASTER);
dp.setMinValue(-127);
dp.setMaxValue(127);
addDatapoint(channel, dp);
dp.setVirtual(false);
}
}
}
}
}
}

View File

@@ -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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.VIRTUAL_DATAPOINT_NAME_INSTALL_MODE_DURATION;
import java.io.IOException;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* A virtual Integer datapoint to hold the duration for the install mode, default one minute.
*
* @author Gerhard Riegler - Initial contribution
*/
public class InstallModeDurationVirtualDatapoint extends AbstractVirtualDatapointHandler {
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_INSTALL_MODE_DURATION;
}
@Override
public void initialize(HmDevice device) {
if (device.isGatewayExtras()) {
HmDatapoint dp = addDatapoint(device, 0, getName(), HmValueType.INTEGER, 60, false);
dp.setMinValue(10);
dp.setMaxValue(300);
}
}
@Override
public boolean canHandleCommand(HmDatapoint dp, Object value) {
return getName().equals(dp.getName());
}
@Override
public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointConfig dpConfig, Object value)
throws IOException, HomematicClientException {
dp.setValue(value);
}
}

View File

@@ -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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
import java.io.IOException;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.misc.MiscUtils;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmInterface;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* A virtual Switch datapoint to start and stop the install mode.
*
* @author Gerhard Riegler - Initial contribution
*/
public class InstallModeVirtualDatapoint extends AbstractVirtualDatapointHandler {
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_INSTALL_MODE;
}
@Override
public void initialize(HmDevice device) {
if (device.isGatewayExtras()) {
addDatapoint(device, 0, getName(), HmValueType.BOOL, Boolean.FALSE, false);
}
}
@Override
public boolean canHandleCommand(HmDatapoint dp, Object value) {
return getName().equals(dp.getName());
}
@Override
public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointConfig dpConfig, Object value)
throws IOException, HomematicClientException {
dp.setValue(value);
boolean enable = MiscUtils.isTrueValue(value);
int duration = getDuration(dp.getChannel());
if (enable) {
gateway.disableDatapoint(dp, duration);
}
HmInterface hmInterface = dp.getChannel().getDevice().getHmInterface();
gateway.getRpcClient(hmInterface).setInstallMode(hmInterface, enable, duration);
}
/**
* Returns the virtual datapoint value for install mode duration.
*/
private Integer getDuration(HmChannel channel) {
HmDatapoint dpDuration = channel
.getDatapoint(HmDatapointInfo.createValuesInfo(channel, VIRTUAL_DATAPOINT_NAME_INSTALL_MODE_DURATION));
return dpDuration == null || dpDuration.getValue() == null || dpDuration.getType() != HmValueType.INTEGER ? 60
: ((Number) dpDuration.getValue()).intValue();
}
}

View File

@@ -0,0 +1,102 @@
/**
* 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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
import java.io.IOException;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.misc.MiscUtils;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmValueType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A virtual Number datapoint which adds a automatic ON_TIME datapoint on supported device. This datapoint sets the
* ON_TIME datapoint every time a STATE or LEVEL datapoint is set, so that the light turns off automatically by the
* device after the specified time.
*
* @author Gerhard Riegler - Initial contribution
*/
public class OnTimeAutomaticVirtualDatapointHandler extends AbstractVirtualDatapointHandler {
private final Logger logger = LoggerFactory.getLogger(OnTimeAutomaticVirtualDatapointHandler.class);
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_ON_TIME_AUTOMATIC;
}
@Override
public void initialize(HmDevice device) {
for (HmChannel channel : device.getChannels()) {
HmDatapointInfo dpInfoOnTime = HmDatapointInfo.createValuesInfo(channel, DATAPOINT_NAME_ON_TIME);
if (channel.hasDatapoint(dpInfoOnTime)) {
HmDatapointInfo dpInfoLevel = HmDatapointInfo.createValuesInfo(channel, DATAPOINT_NAME_LEVEL);
HmDatapointInfo dpInfoState = HmDatapointInfo.createValuesInfo(channel, DATAPOINT_NAME_STATE);
if (channel.hasDatapoint(dpInfoLevel) || channel.hasDatapoint(dpInfoState)) {
HmDatapoint dpOnTime = channel.getDatapoint(dpInfoOnTime);
HmDatapoint dpOnTimeAutomatic = dpOnTime.clone();
dpOnTimeAutomatic.setName(getName());
dpOnTimeAutomatic.setDescription(getName());
addDatapoint(channel, dpOnTimeAutomatic);
}
}
}
}
@Override
public boolean canHandleCommand(HmDatapoint dp, Object value) {
boolean isLevel = DATAPOINT_NAME_LEVEL.equals(dp.getName()) && value != null && value instanceof Number
&& ((Number) value).doubleValue() > 0.0;
boolean isState = DATAPOINT_NAME_STATE.equals(dp.getName()) && MiscUtils.isTrueValue(value);
return ((isLevel || isState) && getVirtualDatapointValue(dp.getChannel()) > 0.0)
|| getName().equals(dp.getName());
}
@Override
public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointConfig dpConfig, Object value)
throws IOException, HomematicClientException {
if (!getName().equals(dp.getName())) {
HmChannel channel = dp.getChannel();
HmDatapoint dpOnTime = channel
.getDatapoint(HmDatapointInfo.createValuesInfo(channel, DATAPOINT_NAME_ON_TIME));
if (dpOnTime != null) {
gateway.sendDatapoint(dpOnTime, new HmDatapointConfig(), getVirtualDatapointValue(channel), null);
} else {
logger.warn(
"Can't find ON_TIME datapoint in channel '{}' in device '{}', ignoring virtual datapoint '{}'",
channel.getNumber(), channel.getDevice().getAddress(), getName());
}
gateway.sendDatapointIgnoreVirtual(dp, dpConfig, value);
} else {
dp.setValue(value);
}
}
/**
* Returns the virtual datapoint value or 0 if not specified.
*/
private Double getVirtualDatapointValue(HmChannel channel) {
HmDatapoint dpOnTimeAutomatic = getVirtualDatapoint(channel);
return dpOnTimeAutomatic == null || dpOnTimeAutomatic.getValue() == null
|| dpOnTimeAutomatic.getType() != HmValueType.FLOAT ? 0.0
: ((Number) dpOnTimeAutomatic.getValue()).doubleValue();
}
}

View File

@@ -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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.VIRTUAL_DATAPOINT_NAME_RELOAD_ALL_FROM_GATEWAY;
import java.io.IOException;
import org.openhab.binding.homematic.internal.communicator.AbstractHomematicGateway;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.misc.MiscUtils;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* A virtual Switch datapoint which reloads all device values from the gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public class ReloadAllFromGatewayVirtualDatapointHandler extends AbstractVirtualDatapointHandler {
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_RELOAD_ALL_FROM_GATEWAY;
}
@Override
public void initialize(HmDevice device) {
if (device.isGatewayExtras()) {
addDatapoint(device, HmChannel.CHANNEL_NUMBER_EXTRAS, getName(), HmValueType.BOOL, Boolean.FALSE, false);
}
}
@Override
public boolean canHandleCommand(HmDatapoint dp, Object value) {
return getName().equals(dp.getName());
}
@Override
public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointConfig dpConfig, Object value)
throws IOException, HomematicClientException {
dp.setValue(value);
if (MiscUtils.isTrueValue(dp.getValue())) {
try {
gateway.getGatewayAdapter().reloadAllDeviceValues();
} finally {
gateway.disableDatapoint(dp, AbstractHomematicGateway.DEFAULT_DISABLE_DELAY);
}
}
}
}

View File

@@ -0,0 +1,60 @@
/**
* 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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.VIRTUAL_DATAPOINT_NAME_RELOAD_FROM_GATEWAY;
import java.io.IOException;
import org.openhab.binding.homematic.internal.communicator.AbstractHomematicGateway;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.misc.MiscUtils;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* A virtual Switch datapoint which reloads all device values from the gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public class ReloadFromGatewayVirtualDatapointHandler extends AbstractVirtualDatapointHandler {
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_RELOAD_FROM_GATEWAY;
}
@Override
public void initialize(HmDevice device) {
addDatapoint(device, 0, getName(), HmValueType.BOOL, Boolean.FALSE, false);
}
@Override
public boolean canHandleCommand(HmDatapoint dp, Object value) {
return getName().equals(dp.getName());
}
@Override
public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointConfig dpConfig, Object value)
throws IOException, HomematicClientException {
dp.setValue(value);
if (MiscUtils.isTrueValue(dp.getValue())) {
try {
gateway.triggerDeviceValuesReload(dp.getChannel().getDevice());
} finally {
gateway.disableDatapoint(dp, AbstractHomematicGateway.DEFAULT_DISABLE_DELAY);
}
}
}
}

View File

@@ -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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.VIRTUAL_DATAPOINT_NAME_RELOAD_RSSI;
import java.io.IOException;
import org.openhab.binding.homematic.internal.communicator.AbstractHomematicGateway;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.misc.MiscUtils;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* A virtual Switch datapoint which loads rssi values for all device from the gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public class ReloadRssiVirtualDatapointHandler extends AbstractVirtualDatapointHandler {
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_RELOAD_RSSI;
}
@Override
public void initialize(HmDevice device) {
if (device.isGatewayExtras()) {
addDatapoint(device, 0, getName(), HmValueType.BOOL, Boolean.FALSE, false);
}
}
@Override
public boolean canHandleCommand(HmDatapoint dp, Object value) {
return getName().equals(dp.getName());
}
@Override
public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointConfig dpConfig, Object value)
throws IOException, HomematicClientException {
dp.setValue(value);
if (MiscUtils.isTrueValue(dp.getValue())) {
try {
gateway.loadRssiValues();
} finally {
gateway.disableDatapoint(dp, AbstractHomematicGateway.DEFAULT_DISABLE_DELAY);
}
}
}
}

View File

@@ -0,0 +1,86 @@
/**
* 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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* A virtual datapoint that unifies the device and peer rssi datapoints.
*
* @author Gerhard Riegler - Initial contribution
*/
public class RssiVirtualDatapointHandler extends AbstractVirtualDatapointHandler {
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_RSSI;
}
@Override
public void initialize(HmDevice device) {
if (isWirelessDevice(device)) {
HmDatapoint dp = addDatapoint(device, 0, getName(), HmValueType.INTEGER, getRssiValue(device.getChannel(0)),
true);
dp.setUnit("dBm");
dp.setMinValue(Integer.MIN_VALUE);
dp.setMaxValue(Integer.MAX_VALUE);
}
}
@Override
public boolean canHandleEvent(HmDatapoint dp) {
return isWirelessDevice(dp.getChannel().getDevice())
&& (DATAPOINT_NAME_RSSI_DEVICE.equals(dp.getName()) || DATAPOINT_NAME_RSSI_PEER.equals(dp.getName()));
}
@Override
public void handleEvent(VirtualGateway gateway, HmDatapoint dp) {
HmChannel channel = dp.getChannel();
Object value = getRssiValue(channel);
HmDatapoint vdpRssi = getVirtualDatapoint(channel);
vdpRssi.setValue(value);
}
/**
* Returns either the device or the peer rssi value.
*/
protected Integer getRssiValue(HmChannel channel) {
HmDatapoint dpRssiDevice = channel.getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_RSSI_DEVICE);
HmDatapoint dpRssiPeer = channel.getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_RSSI_PEER);
Integer deviceValue = getDatapointValue(dpRssiDevice);
Integer peerValue = getDatapointValue(dpRssiPeer);
if ((deviceValue == null || deviceValue == 0) && peerValue != null) {
return peerValue;
}
return deviceValue;
}
private Integer getDatapointValue(HmDatapoint dp) {
if (dp == null || dp.getValue() == null || (Integer) dp.getValue() == 0) {
return null;
}
return (Integer) dp.getValue();
}
protected boolean isWirelessDevice(HmDevice device) {
return device.getChannel(0).getDatapoint(HmParamsetType.VALUES, DATAPOINT_NAME_RSSI_DEVICE) != null;
}
}

View File

@@ -0,0 +1,60 @@
/**
* 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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.VIRTUAL_DATAPOINT_NAME_SIGNAL_STRENGTH;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* A virtual datapoint that represents signal strength of a device as a number with values 0, 1, 2, 3 or 4, 0 being
* worst strength and 4 being best strength.
*
* @author Gerhard Riegler - Initial contribution
*/
public class SignalStrengthVirtualDatapointHandler extends RssiVirtualDatapointHandler {
private static final int RSSI_START = 40;
private static final int RSSI_STEP = 25;
private static final int RSSI_UNITS = 4;
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_SIGNAL_STRENGTH;
}
@Override
public void initialize(HmDevice device) {
if (isWirelessDevice(device)) {
addDatapoint(device, 0, getName(), HmValueType.INTEGER, getRssiValue(device.getChannel(0)), true);
}
}
@Override
public void handleEvent(VirtualGateway gateway, HmDatapoint dp) {
HmChannel channel = dp.getChannel();
Integer value = getRssiValue(channel);
if (value != null) {
Integer strength = Math.max(Math.abs(value), RSSI_START);
strength = strength > RSSI_START + RSSI_STEP * RSSI_UNITS ? 0
: RSSI_UNITS - ((strength - RSSI_START) / RSSI_STEP);
HmDatapoint vdpRssi = getVirtualDatapoint(channel);
vdpRssi.setValue(strength);
}
}
}

View File

@@ -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.homematic.internal.communicator.virtual;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmValueType;
/**
* A virtual datapoint that converts the ENUM state of the HMIP-SWDO device to a contact.
*
* @author Gerhard Riegler - Initial contribution
*/
public class StateContactVirtualDatapointHandler extends AbstractVirtualDatapointHandler {
@Override
public String getName() {
return VIRTUAL_DATAPOINT_NAME_STATE_CONTACT;
}
@Override
public void initialize(HmDevice device) {
if (isApplicable(device)) {
HmChannel channelOne = device.getChannel(1);
if (channelOne != null) {
HmDatapointInfo dpStateInfo = HmDatapointInfo.createValuesInfo(channelOne, DATAPOINT_NAME_STATE);
HmDatapoint dpState = channelOne.getDatapoint(dpStateInfo);
if (dpState != null) {
addDatapoint(device, 1, getName(), HmValueType.BOOL, convertState(dpState.getValue()), true);
}
}
}
}
@Override
public boolean canHandleEvent(HmDatapoint dp) {
return isApplicable(dp.getChannel().getDevice()) && DATAPOINT_NAME_STATE.equals(dp.getName());
}
@Override
public void handleEvent(VirtualGateway gateway, HmDatapoint dp) {
Object value = convertState(dp.getValue());
HmDatapoint vdp = getVirtualDatapoint(dp.getChannel());
vdp.setValue(value);
}
private boolean isApplicable(HmDevice device) {
return device.getType().toUpperCase().startsWith("HMIP-SWDO");
}
private Boolean convertState(Object value) {
if (!(value instanceof Integer)) {
return null;
}
if ((int) value == 0) {
return true;
} else if ((int) value == 1) {
return false;
} else {
return null;
}
}
}

View File

@@ -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.homematic.internal.communicator.virtual;
import java.io.IOException;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDevice;
/**
* Describes the methods used for a virtual datapoint. A virtual datapoint is generated datapoint with special
* functions.
*
* @author Gerhard Riegler - Initial contribution
*/
public interface VirtualDatapointHandler {
/**
* Returns the virtual datapoint name.
*/
public String getName();
/**
* Adds the virtual datapoint to the device.
*/
public void initialize(HmDevice device);
/**
* Returns true, if the virtual datapoint can handle a command for the given datapoint.
*/
public boolean canHandleCommand(HmDatapoint dp, Object value);
/**
* Handles the special functionality for the given virtual datapoint.
*/
public void handleCommand(VirtualGateway gateway, HmDatapoint dp, HmDatapointConfig dpConfig, Object value)
throws IOException, HomematicClientException;
/**
* Returns true, if the virtual datapoint can handle the event for the given datapoint.
*/
public boolean canHandleEvent(HmDatapoint dp);
/**
* Handles a event to extract data required for the virtual datapoint.
*/
public void handleEvent(VirtualGateway gateway, HmDatapoint dp);
/**
* Returns the virtual datapoint in the given channel.
*/
public HmDatapoint getVirtualDatapoint(HmChannel channel);
}

View File

@@ -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.homematic.internal.communicator.virtual;
import java.io.IOException;
import org.openhab.binding.homematic.internal.communicator.HomematicGateway;
import org.openhab.binding.homematic.internal.communicator.HomematicGatewayAdapter;
import org.openhab.binding.homematic.internal.communicator.client.RpcClient;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmInterface;
/**
* Extends the HomematicGateway with a method called from a virtual datapoint.
*
* @author Gerhard Riegler - Initial contribution
*/
public interface VirtualGateway extends HomematicGateway {
/**
* Sends the datapoint from a virtual datapoint.
*/
public void sendDatapointIgnoreVirtual(HmDatapoint dp, HmDatapointConfig dpConfig, Object newValue)
throws IOException, HomematicClientException;
/**
* Returns the rpc client.
*/
public RpcClient<?> getRpcClient(HmInterface hmInterface) throws IOException;
/**
* Disables a boolean datapoint by setting the value to false after a given delay.
*/
public void disableDatapoint(HmDatapoint dp, double delay);
/**
* Returns the event listener.
*/
public HomematicGatewayAdapter getGatewayAdapter();
}

View File

@@ -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.homematic.internal.converter;
/**
* Exception if something goes wrong when converting values between openHAB and the binding.
*
* @author Gerhard Riegler - Initial contribution
*/
public class ConverterException extends Exception {
private static final long serialVersionUID = 78045670450002L;
public ConverterException(String message) {
super(message);
}
}

View File

@@ -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.homematic.internal.converter;
import static org.openhab.binding.homematic.internal.HomematicBindingConstants.*;
import java.util.HashMap;
import java.util.Map;
import org.openhab.binding.homematic.internal.converter.type.DecimalTypeConverter;
import org.openhab.binding.homematic.internal.converter.type.OnOffTypeConverter;
import org.openhab.binding.homematic.internal.converter.type.OpenClosedTypeConverter;
import org.openhab.binding.homematic.internal.converter.type.PercentTypeConverter;
import org.openhab.binding.homematic.internal.converter.type.QuantityTypeConverter;
import org.openhab.binding.homematic.internal.converter.type.StringTypeConverter;
/**
* A factory for creating converters based on the itemType.
*
* @author Gerhard Riegler - Initial contribution
* @author Michael Reitler - QuantityType support
*/
public class ConverterFactory {
private static Map<String, TypeConverter<?>> converterCache = new HashMap<>();
/**
* Returns the converter for a itemType.
*/
public static TypeConverter<?> createConverter(String itemType) throws ConverterException {
Class<? extends TypeConverter<?>> converterClass = null;
if (itemType.startsWith(ITEM_TYPE_NUMBER + ":")) {
converterClass = QuantityTypeConverter.class;
} else {
switch (itemType) {
case ITEM_TYPE_SWITCH:
converterClass = OnOffTypeConverter.class;
break;
case ITEM_TYPE_ROLLERSHUTTER:
case ITEM_TYPE_DIMMER:
converterClass = PercentTypeConverter.class;
break;
case ITEM_TYPE_CONTACT:
converterClass = OpenClosedTypeConverter.class;
break;
case ITEM_TYPE_STRING:
converterClass = StringTypeConverter.class;
break;
case ITEM_TYPE_NUMBER:
converterClass = DecimalTypeConverter.class;
break;
}
}
TypeConverter<?> converter = null;
if (converterClass != null) {
converter = converterCache.get(converterClass.getName());
if (converter == null) {
try {
converter = converterClass.getConstructor().newInstance();
converterCache.put(converterClass.getName(), converter);
} catch (Exception e) {
// ignore
}
}
}
if (converter == null) {
throw new ConverterException("Can't find a converter for type '" + itemType + "'");
}
return converter;
}
}

View File

@@ -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.homematic.internal.converter;
/**
* Exception if converting between two types is not possible due wrong item type or command.
*
* @author Gerhard Riegler - Initial contribution
*/
public class ConverterTypeException extends ConverterException {
private static final long serialVersionUID = 7114173349077221055L;
public ConverterTypeException(String message) {
super(message);
}
}

View File

@@ -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.homematic.internal.converter;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
/**
* Holds device specific infos for state invertion.
*
* @author Gerhard Riegler - Initial contribution
*/
public class StateInvertInfo {
private String deviceType;
private int minChannel;
private int maxChannel;
/**
* Creates a StateInvertInfo with the specified deviceType.
*/
public StateInvertInfo(String deviceType) {
this(deviceType, -1, -1);
}
/**
* Creates a StateInvertInfo with the specified deviceType and a channel range.
*/
public StateInvertInfo(String deviceType, int minChannel, int maxChannel) {
this.deviceType = deviceType;
this.minChannel = minChannel;
this.maxChannel = maxChannel;
}
/**
* Validates if the state of a datapoint must be inverted.
*/
public boolean isToInvert(HmDatapoint dp) {
String dpDeviceType = dp.getChannel().getDevice().getType();
if (minChannel != -1) {
int dpChannelNumber = dp.getChannel().getNumber();
return dpDeviceType.startsWith(deviceType) && dpChannelNumber >= minChannel
&& dpChannelNumber <= maxChannel;
} else {
return dpDeviceType.startsWith(deviceType);
}
}
}

View File

@@ -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.homematic.internal.converter;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
/**
* Converter interface for converting between openHAB states/commands and Homematic values.
*
* @author Gerhard Riegler - Initial contribution
*/
public interface TypeConverter<T extends State> {
/**
* Converts a openHAB type to a Homematic value.
*/
public Object convertToBinding(Type type, HmDatapoint dp) throws ConverterException;
/**
* Converts a Homematic value to a openHAB type.
*/
public T convertFromBinding(HmDatapoint dp) throws ConverterException;
}

View File

@@ -0,0 +1,194 @@
/**
* 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.homematic.internal.converter.type;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import org.openhab.binding.homematic.internal.converter.ConverterException;
import org.openhab.binding.homematic.internal.converter.ConverterTypeException;
import org.openhab.binding.homematic.internal.converter.StateInvertInfo;
import org.openhab.binding.homematic.internal.converter.TypeConverter;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for all Converters with common methods.
*
* @author Gerhard Riegler - Initial contribution
*/
public abstract class AbstractTypeConverter<T extends State> implements TypeConverter<T> {
private final Logger logger = LoggerFactory.getLogger(AbstractTypeConverter.class);
/**
* Defines all devices where the state datapoint must be inverted.
*/
private static final List<StateInvertInfo> stateInvertDevices = new ArrayList<>(3);
static {
stateInvertDevices.add(new StateInvertInfo(DEVICE_TYPE_SHUTTER_CONTACT));
stateInvertDevices.add(new StateInvertInfo(DEVICE_TYPE_SHUTTER_CONTACT_2));
stateInvertDevices.add(new StateInvertInfo(DEVICE_TYPE_INCLINATION_SENSOR));
stateInvertDevices.add(new StateInvertInfo(DEVICE_TYPE_WIRED_IO_MODULE, 15, 26));
stateInvertDevices.add(new StateInvertInfo(DEVICE_TYPE_MAX_WINDOW_SENSOR));
stateInvertDevices.add(new StateInvertInfo(DEVICE_TYPE_SHUTTER_CONTACT_INTERFACE));
}
/**
* Checks the datapoint if the state value must be inverted.
*/
protected boolean isStateInvertDatapoint(HmDatapoint dp) {
if (DATAPOINT_NAME_STATE.equals(dp.getName())) {
for (StateInvertInfo stateInvertInfo : stateInvertDevices) {
if (stateInvertInfo.isToInvert(dp)) {
return true;
}
}
}
return false;
}
/**
* Rounds a double value.
*/
protected BigDecimal round(Double number) {
BigDecimal bd = new BigDecimal(number == null ? "0" : number.toString());
String stringBd = bd.toPlainString();
int scale = stringBd.length() - (stringBd.lastIndexOf('.') + 1);
return bd.setScale(scale > 2 ? 6 : 2, RoundingMode.HALF_UP);
}
@SuppressWarnings("unchecked")
@Override
public Object convertToBinding(Type type, HmDatapoint dp) throws ConverterException {
if (isLoggingRequired()) {
logAtDefaultLevel(
"Converting type {} with value '{}' using {} to datapoint '{}' (dpType='{}', dpUnit='{}')",
type.getClass().getSimpleName(), type.toString(), this.getClass().getSimpleName(),
new HmDatapointInfo(dp), dp.getType(), dp.getUnit());
}
if (type == UnDefType.NULL) {
return null;
} else if (type.getClass().isEnum() && !(this instanceof OnOffTypeConverter)
&& !(this instanceof OpenClosedTypeConverter)) {
return commandToBinding((Command) type, dp);
} else if (!toBindingValidation(dp, type.getClass())) {
String errorMessage = String.format("Can't convert type %s with value '%s' to %s value with %s for '%s'",
type.getClass().getSimpleName(), type.toString(), dp.getType(), this.getClass().getSimpleName(),
new HmDatapointInfo(dp));
throw new ConverterTypeException(errorMessage);
}
return toBinding((T) type, dp);
}
@SuppressWarnings("unchecked")
@Override
public T convertFromBinding(HmDatapoint dp) throws ConverterException {
if (isLoggingRequired()) {
logAtDefaultLevel("Converting datapoint '{}' (dpType='{}', dpUnit='{}', dpValue='{}') with {}",
new HmDatapointInfo(dp), dp.getType(), dp.getUnit(), dp.getValue(),
this.getClass().getSimpleName());
}
if (dp.getValue() == null) {
return (T) UnDefType.NULL;
} else if (!fromBindingValidation(dp)) {
String errorMessage = String.format("Can't convert %s value '%s' with %s for '%s'", dp.getType(),
dp.getValue(), this.getClass().getSimpleName(), new HmDatapointInfo(dp));
throw new ConverterTypeException(errorMessage);
}
return fromBinding(dp);
}
/**
* By default, instances of {@link AbstractTypeConverter} log in level TRACE.
* May be overridden to increase logging verbosity of a converter.
*
* @return desired LogLevel
*/
protected LogLevel getDefaultLogLevelForTypeConverter() {
return LogLevel.TRACE;
}
private boolean isLoggingRequired() {
if (getDefaultLogLevelForTypeConverter() == LogLevel.TRACE) {
return logger.isTraceEnabled();
}
if (getDefaultLogLevelForTypeConverter() == LogLevel.DEBUG) {
return logger.isDebugEnabled();
}
return true;
}
private void logAtDefaultLevel(String format, Object... arguments) {
switch (getDefaultLogLevelForTypeConverter()) {
case TRACE:
logger.trace(format, arguments);
break;
case DEBUG:
logger.debug(format, arguments);
break;
case INFO:
default:
logger.info(format, arguments);
break;
}
}
/**
* Converts a openHAB command to a Homematic value.
*/
protected Object commandToBinding(Command command, HmDatapoint dp) throws ConverterException {
throw new ConverterException("Unsupported command " + command.getClass().getSimpleName() + " for "
+ this.getClass().getSimpleName());
}
/**
* Returns true, if the conversion from openHAB to the binding is possible.
*/
protected abstract boolean toBindingValidation(HmDatapoint dp, Class<? extends Type> typeClass);
/**
* Converts the type to a datapoint value.
*/
protected abstract Object toBinding(T type, HmDatapoint dp) throws ConverterException;
/**
* Returns true, if the conversion from the binding to openHAB is possible.
*/
protected abstract boolean fromBindingValidation(HmDatapoint dp);
/**
* Converts the datapoint value to a openHAB type.
*/
protected abstract T fromBinding(HmDatapoint dp) throws ConverterException;
protected enum LogLevel {
TRACE,
INFO,
DEBUG
}
}

View File

@@ -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.homematic.internal.converter.type;
import java.math.BigDecimal;
import org.openhab.binding.homematic.internal.converter.ConverterException;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.types.Type;
/**
* Converts between a Homematic datapoint value and a openHAB DecimalType.
*
* @author Gerhard Riegler - Initial contribution
*/
public class DecimalTypeConverter extends AbstractTypeConverter<DecimalType> {
@Override
protected boolean toBindingValidation(HmDatapoint dp, Class<? extends Type> typeClass) {
return dp.isNumberType() && typeClass.isAssignableFrom(DecimalType.class);
}
@Override
protected Object toBinding(DecimalType type, HmDatapoint dp) throws ConverterException {
if (dp.isIntegerType()) {
return type.intValue();
}
return round(type.doubleValue()).doubleValue();
}
@Override
protected boolean fromBindingValidation(HmDatapoint dp) {
return dp.isNumberType() && dp.getValue() instanceof Number;
}
@Override
protected DecimalType fromBinding(HmDatapoint dp) throws ConverterException {
Number number = ((Number) dp.getValue()).doubleValue();
if (dp.isIntegerType()) {
return new DecimalType(new BigDecimal(number.intValue()));
}
return new DecimalType(round(number.doubleValue()));
}
}

View File

@@ -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.homematic.internal.converter.type;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.DATAPOINT_NAME_SENSOR;
import org.openhab.binding.homematic.internal.converter.ConverterException;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.types.Type;
/**
* Converts between a Homematic datapoint value and a openHAB OnOffType.
*
* @author Gerhard Riegler - Initial contribution
*/
public class OnOffTypeConverter extends AbstractTypeConverter<OnOffType> {
@Override
protected boolean toBindingValidation(HmDatapoint dp, Class<? extends Type> typeClass) {
return dp.isBooleanType() && typeClass.isAssignableFrom(OnOffType.class);
}
@Override
protected Object toBinding(OnOffType type, HmDatapoint dp) throws ConverterException {
return (type == OnOffType.OFF ? Boolean.FALSE : Boolean.TRUE) != isInvert(dp);
}
@Override
protected boolean fromBindingValidation(HmDatapoint dp) {
return dp.isBooleanType() && dp.getValue() instanceof Boolean;
}
@Override
protected OnOffType fromBinding(HmDatapoint dp) throws ConverterException {
return (((Boolean) dp.getValue()) == Boolean.FALSE) != isInvert(dp) ? OnOffType.OFF : OnOffType.ON;
}
/**
* If the item is a sensor or a state from some devices, then OnOff must be inverted.
*/
private boolean isInvert(HmDatapoint dp) {
return DATAPOINT_NAME_SENSOR.equals(dp.getName()) || isStateInvertDatapoint(dp);
}
}

View File

@@ -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.homematic.internal.converter.type;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.DATAPOINT_NAME_SENSOR;
import org.openhab.binding.homematic.internal.converter.ConverterException;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.types.Type;
/**
* Converts between a Homematic datapoint value and a openHAB OpenClosedType.
*
* @author Gerhard Riegler - Initial contribution
*/
public class OpenClosedTypeConverter extends AbstractTypeConverter<OpenClosedType> {
@Override
protected boolean toBindingValidation(HmDatapoint dp, Class<? extends Type> typeClass) {
return dp.isBooleanType() && typeClass.isAssignableFrom(OpenClosedType.class);
}
@Override
protected Object toBinding(OpenClosedType type, HmDatapoint dp) throws ConverterException {
return (type == OpenClosedType.CLOSED ? Boolean.FALSE : Boolean.TRUE) != isInvert(dp);
}
@Override
protected boolean fromBindingValidation(HmDatapoint dp) {
return dp.isBooleanType() && dp.getValue() instanceof Boolean;
}
@Override
protected OpenClosedType fromBinding(HmDatapoint dp) throws ConverterException {
return (((Boolean) dp.getValue()) == Boolean.FALSE) != isInvert(dp) ? OpenClosedType.CLOSED
: OpenClosedType.OPEN;
}
/**
* Invert only values which are not from a sensor or a state from some devices.
*/
private boolean isInvert(HmDatapoint dp) {
return !DATAPOINT_NAME_SENSOR.equals(dp.getName()) && !isStateInvertDatapoint(dp);
}
}

View File

@@ -0,0 +1,110 @@
/**
* 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.homematic.internal.converter.type;
import org.openhab.binding.homematic.internal.converter.ConverterException;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmInterface;
import org.openhab.binding.homematic.internal.type.MetadataUtils;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Command;
import org.openhab.core.types.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Converts between a Homematic datapoint value and a openHAB PercentType.
*
* @author Gerhard Riegler - Initial contribution
*/
public class PercentTypeConverter extends AbstractTypeConverter<PercentType> {
private final Logger logger = LoggerFactory.getLogger(PercentTypeConverter.class);
@Override
protected Object commandToBinding(Command command, HmDatapoint dp) throws ConverterException {
if (command.getClass() == IncreaseDecreaseType.class) {
PercentType type = convertFromBinding(dp);
int percent = type.intValue();
percent += command.equals(IncreaseDecreaseType.INCREASE) ? 10 : -10;
percent = (percent / 10) * 10;
percent = Math.min(100, percent);
percent = Math.max(0, percent);
return convertToBinding(new PercentType(percent), dp);
} else if (command.getClass() == OnOffType.class) {
PercentType type = new PercentType(command.equals(OnOffType.ON) ? 100 : 0);
return convertToBinding(type, dp);
} else if (command.getClass() == UpDownType.class) {
return convertToBinding(command.equals(UpDownType.UP) ? PercentType.ZERO : PercentType.HUNDRED, dp);
} else {
return super.commandToBinding(command, dp);
}
}
private double getCorrectedMaxValue(HmDatapoint dp) {
double max = dp.getMaxValue().doubleValue();
return (max == 1.01 && dp.getChannel().getDevice().getHmInterface() == HmInterface.HMIP ? 1.0d : max);
}
@Override
protected boolean toBindingValidation(HmDatapoint dp, Class<? extends Type> typeClass) {
return dp.isNumberType() && dp.getMaxValue() != null && dp.getMinValue() != null
&& dp.getChannel().getType() != null && typeClass.isAssignableFrom(PercentType.class);
}
@Override
protected Object toBinding(PercentType type, HmDatapoint dp) throws ConverterException {
double maxValue = getCorrectedMaxValue(dp);
Double number = (type.doubleValue() / 100) * maxValue;
if (MetadataUtils.isRollerShutter(dp)) {
if (type == PercentType.HUNDRED) { // means DOWN
return dp.getMinValue().doubleValue();
} else if (type == PercentType.ZERO) { // means UP
return maxValue;
}
return maxValue - number;
}
if (number < 0.0 || number > 100.0) {
logger.warn("Percent value '{}' out of range, truncating value for {}", number, dp);
number = number < 0.0 ? 0.0 : 100.0;
}
if (dp.isIntegerType()) {
return number.intValue();
}
return round(number).doubleValue();
}
@Override
protected boolean fromBindingValidation(HmDatapoint dp) {
return dp.isNumberType() && dp.getValue() instanceof Number && dp.getMaxValue() != null
&& dp.getChannel().getType() != null;
}
@Override
protected PercentType fromBinding(HmDatapoint dp) throws ConverterException {
Double number = ((Number) dp.getValue()).doubleValue();
int percent = (int) (100 / getCorrectedMaxValue(dp) * number);
if (percent > 100) {
logger.warn("Percent value '{}' out of range, truncating value for {}", number, dp);
percent = 100;
}
if (MetadataUtils.isRollerShutter(dp)) {
percent = 100 - percent;
}
return new PercentType(percent);
}
}

View File

@@ -0,0 +1,149 @@
/**
* 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.homematic.internal.converter.type;
import java.math.BigDecimal;
import javax.measure.Quantity;
import org.openhab.binding.homematic.internal.converter.ConverterException;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.MetricPrefix;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.types.Type;
/**
* Converts between a Homematic datapoint value and a {@link DecimalType}.
*
* @author Michael Reitler - Initial contribution
*/
public class QuantityTypeConverter extends AbstractTypeConverter<QuantityType<? extends Quantity<?>>> {
// this literal is required because some gateway types are mixing up encodings in their XML-RPC responses
private final String UNCORRECT_ENCODED_CELSIUS = "°C";
// "100%" is a commonly used "unit" in datapoints. Generated channel-type is of DecimalType,
// but clients may define a QuantityType if preferred
private final String HUNDRED_PERCENT = "100%";
@Override
protected boolean toBindingValidation(HmDatapoint dp, Class<? extends Type> typeClass) {
return dp.isNumberType() && typeClass.isAssignableFrom(QuantityType.class);
}
@Override
protected Object toBinding(QuantityType<? extends Quantity<?>> type, HmDatapoint dp) throws ConverterException {
if (dp.isIntegerType()) {
return toUnitFromDatapoint(type, dp).intValue();
}
return round(toUnitFromDatapoint(type, dp).doubleValue()).doubleValue();
}
private QuantityType<? extends Quantity<?>> toUnitFromDatapoint(QuantityType<? extends Quantity<?>> type,
HmDatapoint dp) {
if (dp == null || dp.getUnit() == null || dp.getUnit().isEmpty()) {
// datapoint is dimensionless, nothing to convert
return type;
}
// convert the given QuantityType to a QuantityType with the unit of the target datapoint
switch (dp.getUnit()) {
case "Lux":
return type.toUnit(SmartHomeUnits.LUX);
case "degree":
return type.toUnit(SmartHomeUnits.DEGREE_ANGLE);
case HUNDRED_PERCENT:
return type.toUnit(SmartHomeUnits.ONE);
case UNCORRECT_ENCODED_CELSIUS:
return type.toUnit(SIUnits.CELSIUS);
case "dBm":
case "minutes":
case "day":
case "month":
case "year":
case "":
return type;
default:
// According to datapoint documentation, the following values are remaining
// °C, V, %, s, min, mHz, Hz, hPa, km/h, mm, W, m3
return type.toUnit(dp.getUnit());
}
}
@Override
protected boolean fromBindingValidation(HmDatapoint dp) {
return dp.isNumberType() && dp.getValue() instanceof Number;
}
@Override
protected QuantityType<? extends Quantity<?>> fromBinding(HmDatapoint dp) throws ConverterException {
Number number = null;
if (dp.isIntegerType()) {
number = new BigDecimal(((Number) dp.getValue()).intValue());
} else {
number = round(((Number) dp.getValue()).doubleValue());
}
// create a QuantityType from the datapoint's value based on the datapoint's unit
String unit = dp.getUnit() != null ? dp.getUnit() : "";
switch (unit) {
case UNCORRECT_ENCODED_CELSIUS:
case "°C":
return new QuantityType<>(number, SIUnits.CELSIUS);
case "V":
return new QuantityType<>(number, SmartHomeUnits.VOLT);
case "%":
return new QuantityType<>(number, SmartHomeUnits.PERCENT);
case "mHz":
return new QuantityType<>(number, MetricPrefix.MILLI(SmartHomeUnits.HERTZ));
case "Hz":
return new QuantityType<>(number, SmartHomeUnits.HERTZ);
case "hPa":
return new QuantityType<>(number, SIUnits.PASCAL.multiply(2));
case "Lux":
return new QuantityType<>(number, SmartHomeUnits.LUX);
case "degree":
return new QuantityType<>(number, SmartHomeUnits.DEGREE_ANGLE);
case "km/h":
return new QuantityType<>(number, SIUnits.KILOMETRE_PER_HOUR);
case "mm":
return new QuantityType<>(number, MetricPrefix.MILLI(SIUnits.METRE));
case "W":
return new QuantityType<>(number, SmartHomeUnits.WATT);
case "Wh":
return new QuantityType<>(number, SmartHomeUnits.WATT_HOUR);
case "m3":
return new QuantityType<>(number, SIUnits.CUBIC_METRE);
case HUNDRED_PERCENT:
return new QuantityType<>(number.doubleValue() * 100.0, SmartHomeUnits.PERCENT);
case "dBm":
case "s":
case "min":
case "minutes":
case "day":
case "month":
case "year":
case "":
default:
return new QuantityType<>(number, SmartHomeUnits.ONE);
}
}
@Override
protected LogLevel getDefaultLogLevelForTypeConverter() {
// increase logging verbosity for this type of converter
return LogLevel.DEBUG;
}
}

View File

@@ -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.homematic.internal.converter.type;
import org.openhab.binding.homematic.internal.converter.ConverterException;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Type;
/**
* Converts between a Homematic datapoint value and a openHAB StringType.
*
* @author Gerhard Riegler - Initial contribution
*/
public class StringTypeConverter extends AbstractTypeConverter<StringType> {
@Override
protected boolean toBindingValidation(HmDatapoint dp, Class<? extends Type> typeClass) {
return (dp.isStringType() || dp.isEnumType()) && typeClass.isAssignableFrom(StringType.class);
}
@Override
protected Object toBinding(StringType type, HmDatapoint dp) throws ConverterException {
if (dp.isStringType()) {
return type.toString();
} else {
int idx = dp.getOptionIndex(type.toString());
if (idx == -1) {
throw new ConverterException(String.format("Option value '%s' not found in datapoint '%s'",
type.toString(), new HmDatapointInfo(dp)));
}
return idx;
}
}
@Override
protected boolean fromBindingValidation(HmDatapoint dp) {
return (dp.isStringType()) || (dp.isEnumType() && dp.getValue() instanceof Number);
}
@Override
protected StringType fromBinding(HmDatapoint dp) throws ConverterException {
if (dp.isStringType()) {
return new StringType(String.valueOf(dp.getValue()));
} else {
String value = dp.getOptionValue();
if (value == null) {
throw new ConverterException(String.format("Option for value '%s' not found in datapoint '%s'",
dp.getValue(), new HmDatapointInfo(dp)));
}
return new StringType(value);
}
}
}

View File

@@ -0,0 +1,164 @@
/**
* 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.homematic.internal.discovery;
import static org.openhab.binding.homematic.internal.HomematicBindingConstants.THING_TYPE_BRIDGE;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.SocketTimeoutException;
import java.util.Collections;
import java.util.concurrent.Future;
import org.openhab.binding.homematic.internal.discovery.eq3udp.Eq3UdpRequest;
import org.openhab.binding.homematic.internal.discovery.eq3udp.Eq3UdpResponse;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Discovers Homematic CCU's and adds the results to the inbox.
*
* @author Gerhard Riegler - Initial contribution
*/
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.homematic")
public class CcuDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(CcuDiscoveryService.class);
private static final int RECEIVE_TIMEOUT_MSECS = 3000;
private InetAddress broadcastAddress;
private MulticastSocket socket;
private Future<?> scanFuture;
private NetworkAddressService networkAddressService;
public CcuDiscoveryService() {
super(Collections.singleton(THING_TYPE_BRIDGE), 5, true);
}
@Override
protected void startScan() {
if (scanFuture == null) {
scanFuture = scheduler.submit(this::startDiscovery);
} else {
logger.debug("Homematic CCU background discovery scan in progress");
}
}
@Override
protected void stopScan() {
if (scanFuture != null) {
scanFuture.cancel(false);
scanFuture = null;
}
}
@Override
protected void startBackgroundDiscovery() {
// only start once at startup
startScan();
}
@Override
protected void stopBackgroundDiscovery() {
stopScan();
}
private synchronized void startDiscovery() {
try {
logger.debug("Starting Homematic CCU discovery scan");
String configuredBroadcastAddress = networkAddressService.getConfiguredBroadcastAddress();
if (configuredBroadcastAddress != null) {
broadcastAddress = InetAddress.getByName(configuredBroadcastAddress);
}
if (broadcastAddress == null) {
logger.warn("Homematic CCU discovery: discovery not possible, no broadcast address found");
return;
}
socket = new MulticastSocket();
socket.setBroadcast(true);
socket.setTimeToLive(5);
socket.setSoTimeout(800);
sendBroadcast();
receiveResponses();
} catch (Exception ex) {
logger.error("An error was thrown while running Homematic CCU discovery {}", ex.getMessage(), ex);
} finally {
scanFuture = null;
}
}
/**
* Sends a UDP hello broadcast message for CCU gateways.
*/
private void sendBroadcast() throws IOException {
Eq3UdpRequest hello = new Eq3UdpRequest();
byte[] data = hello.getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length, broadcastAddress, 43439);
socket.send(packet);
}
/**
* Receives the UDP responses to the hello messages.
*/
private void receiveResponses() throws IOException {
long startTime = System.currentTimeMillis();
long currentTime = System.currentTimeMillis();
while (currentTime - startTime < RECEIVE_TIMEOUT_MSECS) {
extractGatewayInfos();
currentTime = System.currentTimeMillis();
}
socket.close();
}
/**
* Extracts the CCU infos from the UDP response.
*/
private void extractGatewayInfos() throws IOException {
try {
DatagramPacket packet = new DatagramPacket(new byte[265], 256);
socket.receive(packet);
Eq3UdpResponse response = new Eq3UdpResponse(packet.getData());
logger.trace("Eq3UdpResponse: {}", response);
if (response.isValid()) {
logger.debug("Discovered a CCU gateway with serial number '{}'", response.getSerialNumber());
String address = packet.getAddress().getHostAddress();
ThingUID thingUid = new ThingUID(THING_TYPE_BRIDGE, response.getSerialNumber());
thingDiscovered(DiscoveryResultBuilder.create(thingUid).withProperty("gatewayAddress", address)
.withRepresentationProperty("gatewayAddress")
.withLabel(response.getDeviceTypeId() + " - " + address).build());
}
} catch (SocketTimeoutException ex) {
// ignore
}
}
@Reference
protected void setNetworkAddressService(NetworkAddressService networkAddressService) {
this.networkAddressService = networkAddressService;
}
protected void unsetNetworkAddressService(NetworkAddressService networkAddressService) {
this.networkAddressService = null;
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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.homematic.internal.discovery;
import static org.openhab.binding.homematic.internal.HomematicBindingConstants.THING_TYPE_BRIDGE;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.jupnp.model.meta.DeviceDetails;
import org.jupnp.model.meta.RemoteDevice;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HomegearDiscoveryParticipant} is responsible for discovering new Homegear gateways on the network.
*
* @author Gerhard Riegler - Initial contribution
*/
@Component(immediate = true)
public class HomegearDiscoveryParticipant implements UpnpDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(HomegearDiscoveryParticipant.class);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Collections.singleton(THING_TYPE_BRIDGE);
}
@Override
public DiscoveryResult createResult(RemoteDevice device) {
ThingUID uid = getThingUID(device);
if (uid != null) {
DeviceDetails details = device.getDetails();
Map<String, Object> properties = new HashMap<>(3);
properties.put("gatewayAddress", details.getBaseURL().getHost());
logger.debug("Discovered a Homegear gateway with serial number '{}'", details.getSerialNumber());
return DiscoveryResultBuilder.create(uid).withProperties(properties)
.withLabel(details.getModelDetails().getModelNumber()).build();
}
return null;
}
@Override
public ThingUID getThingUID(RemoteDevice device) {
DeviceDetails details = device.getDetails();
String modelName = details.getModelDetails().getModelName();
if ("HOMEGEAR".equalsIgnoreCase(modelName)) {
return new ThingUID(THING_TYPE_BRIDGE, details.getSerialNumber());
}
return null;
}
}

View File

@@ -0,0 +1,248 @@
/**
* 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.homematic.internal.discovery;
import static org.openhab.binding.homematic.internal.HomematicBindingConstants.BINDING_ID;
import java.util.Collections;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.HomematicGateway;
import org.openhab.binding.homematic.internal.handler.HomematicBridgeHandler;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.type.UidUtils;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HomematicDeviceDiscoveryService} is used to discover devices that are connected to a Homematic gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public class HomematicDeviceDiscoveryService extends AbstractDiscoveryService
implements DiscoveryService, ThingHandlerService {
private final Logger logger = LoggerFactory.getLogger(HomematicDeviceDiscoveryService.class);
private static final int DISCOVER_TIMEOUT_SECONDS = 300;
private @NonNullByDefault({}) HomematicBridgeHandler bridgeHandler;
private Future<?> loadDevicesFuture;
private volatile boolean isInInstallMode = false;
private volatile Object installModeSync = new Object();
private volatile int installModeDuration = HomematicConfig.DEFAULT_INSTALL_MODE_DURATION;
public HomematicDeviceDiscoveryService() {
super(Collections.singleton(new ThingTypeUID(BINDING_ID, "-")), DISCOVER_TIMEOUT_SECONDS, false);
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof HomematicBridgeHandler) {
this.bridgeHandler = (HomematicBridgeHandler) handler;
this.bridgeHandler.setDiscoveryService(this);
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return bridgeHandler;
}
/**
* Called on component activation.
*/
@Override
public void activate() {
super.activate(null);
}
@Override
public void deactivate() {
super.deactivate();
}
@Override
protected void startScan() {
logger.debug("Starting Homematic discovery scan");
enableInstallMode();
loadDevices();
}
/**
* Will set controller in <i>installMode==true</i>, but only if the bridge
* is ONLINE (e.g. not during INITIALIZATION).
*/
private void enableInstallMode() {
try {
HomematicGateway gateway = bridgeHandler.getGateway();
ThingStatus bridgeStatus = null;
if (bridgeHandler != null) {
Thing bridge = bridgeHandler.getThing();
bridgeStatus = bridge.getStatus();
updateInstallModeDuration(bridge);
}
if (ThingStatus.ONLINE == bridgeStatus) {
gateway.setInstallMode(true, installModeDuration);
int remaining = gateway.getInstallMode();
if (remaining > 0) {
setIsInInstallMode();
logger.debug("Successfully put controller in install mode. Remaining time: {} seconds", remaining);
} else {
logger.warn("Controller did not accept requested install mode");
}
} else {
logger.debug("Will not attempt to set controller in install mode, because bridge is not ONLINE.");
}
} catch (Exception ex) {
logger.warn("Failed to set Homematic controller in install mode", ex);
}
}
private void updateInstallModeDuration(Thing bridge) {
HomematicConfig config = bridge.getConfiguration().as(HomematicConfig.class);
installModeDuration = config.getInstallModeDuration();
}
@Override
public int getScanTimeout() {
return installModeDuration;
}
@Override
public synchronized void stopScan() {
logger.debug("Stopping Homematic discovery scan");
if (bridgeHandler != null && bridgeHandler.getGateway() != null) {
disableInstallMode();
bridgeHandler.getGateway().cancelLoadAllDeviceMetadata();
}
waitForScanFinishing();
super.stopScan();
}
private void disableInstallMode() {
try {
synchronized (installModeSync) {
if (isInInstallMode) {
isInInstallMode = false;
installModeSync.notify();
bridgeHandler.getGateway().setInstallMode(false, 0);
}
}
} catch (Exception ex) {
logger.warn("Failed to disable Homematic controller's install mode", ex);
}
}
private void setIsInInstallMode() {
synchronized (installModeSync) {
isInInstallMode = true;
}
}
private void waitForInstallModeFinished(int timeout) throws InterruptedException {
synchronized (installModeSync) {
while (isInInstallMode) {
installModeSync.wait(timeout);
}
}
}
private void waitForLoadDevicesFinished() throws InterruptedException, ExecutionException {
if (loadDevicesFuture != null) {
loadDevicesFuture.get();
}
}
/**
* Waits for the discovery scan to finish and then returns.
*/
public void waitForScanFinishing() {
logger.debug("Waiting for finishing Homematic device discovery scan");
try {
waitForInstallModeFinished(DISCOVER_TIMEOUT_SECONDS * 1000);
waitForLoadDevicesFinished();
} catch (ExecutionException | InterruptedException ex) {
// ignore
} catch (Exception ex) {
logger.error("Error waiting for device discovery scan: {}", ex.getMessage(), ex);
}
String gatewayId = bridgeHandler != null && bridgeHandler.getGateway() != null
? bridgeHandler.getGateway().getId()
: "UNKNOWN";
logger.debug("Finished Homematic device discovery scan on gateway '{}'", gatewayId);
}
/**
* Starts a thread which loads all Homematic devices connected to the gateway.
*/
public void loadDevices() {
if (loadDevicesFuture == null && bridgeHandler.getGateway() != null) {
loadDevicesFuture = scheduler.submit(() -> {
try {
final HomematicGateway gateway = bridgeHandler.getGateway();
gateway.loadAllDeviceMetadata();
bridgeHandler.getTypeGenerator().validateFirmwares();
} catch (Throwable ex) {
logger.error("{}", ex.getMessage(), ex);
} finally {
loadDevicesFuture = null;
bridgeHandler.setOfflineStatus();
removeOlderResults(getTimestampOfLastScan());
}
});
} else {
logger.debug("Homematic devices discovery scan in progress");
}
}
/**
* Removes the Homematic device.
*/
public void deviceRemoved(HmDevice device) {
ThingUID thingUID = UidUtils.generateThingUID(device, bridgeHandler.getThing());
thingRemoved(thingUID);
}
/**
* Generates the DiscoveryResult from a Homematic device.
*/
public void deviceDiscovered(HmDevice device) {
ThingUID bridgeUID = bridgeHandler.getThing().getUID();
ThingTypeUID typeUid = UidUtils.generateThingTypeUID(device);
ThingUID thingUID = new ThingUID(typeUid, bridgeUID, device.getAddress());
String label = device.getName() != null ? device.getName() : device.getAddress();
long timeToLive = bridgeHandler.getThing().getConfiguration().as(HomematicConfig.class)
.getDiscoveryTimeToLive();
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID).withLabel(label)
.withProperty(Thing.PROPERTY_SERIAL_NUMBER, device.getAddress())
.withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).withTTL(timeToLive).build();
thingDiscovered(discoveryResult);
}
}

View File

@@ -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.homematic.internal.discovery.eq3udp;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Random;
/**
* Generates a UDP request to discover Homematic CCU gateways.
*
* @author Gerhard Riegler - Initial contribution
*/
public class Eq3UdpRequest {
private static final byte UDP_IDENTIFY = 73;
private static final byte UDP_SEPARATOR = 0;
private static final int senderId = new Random().nextInt() & 0xFFFFFF;
private static final String EQ3_DEVICE_TYPE = "eQ3-*";
private static final String EQ3_SERIAL_NUMBER = "*";
/**
* Returns the Eq3 Serialnumber.
*/
public static String getEq3SerialNumber() {
return EQ3_SERIAL_NUMBER;
}
/**
* Returns the sender id.
*/
public static int getSenderId() {
return senderId;
}
/**
* Creates the UDP request.
*/
public byte[] getBytes() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(2);
for (int i = 2; i >= 0; i--) {
byte temp = (byte) (senderId >> i * 8 & 0xFF);
baos.write(temp);
}
baos.write(UDP_SEPARATOR);
baos.write(EQ3_DEVICE_TYPE.getBytes());
baos.write(UDP_SEPARATOR);
baos.write(EQ3_SERIAL_NUMBER.getBytes());
baos.write(UDP_SEPARATOR);
baos.write(UDP_IDENTIFY);
return baos.toByteArray();
}
}

View File

@@ -0,0 +1,92 @@
/**
* 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.homematic.internal.discovery.eq3udp;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
/**
* Extracts a UDP response from a Homematic CCU gateway.
*
* @author Gerhard Riegler - Initial contribution
*/
public class Eq3UdpResponse {
private int senderId;
private String deviceTypeId;
private String serialNumber;
/**
* Extracts the received UDP response.
*/
public Eq3UdpResponse(byte[] buffer) throws IndexOutOfBoundsException {
int index = 0;
byte protocolVersion = buffer[(index++)];
if (protocolVersion == 2) {
senderId = readInt(buffer, index, 3);
index += 4;
}
deviceTypeId = readString(buffer, index++);
index += deviceTypeId.length();
serialNumber = readString(buffer, index);
}
/**
* Returns the device type of the gateway.
*/
public String getDeviceTypeId() {
return deviceTypeId;
}
/**
* Returns the serial number of the gateway.
*/
public String getSerialNumber() {
return serialNumber;
}
/**
* Returns true, if this response is from a Homematic CCU gateway.
*/
public boolean isValid() {
return this.senderId == Eq3UdpRequest.getSenderId()
&& (deviceTypeId.startsWith("eQ3-HM-CCU") || deviceTypeId.startsWith("eQ3-HmIP-CCU3"))
&& !serialNumber.contains(Eq3UdpRequest.getEq3SerialNumber());
}
private String readString(byte[] data, int index) throws IndexOutOfBoundsException {
String result = "";
for (int i = index; i < data.length; i++) {
if (data[i] == 0) {
break;
}
result += (char) data[i];
}
return result;
}
private int readInt(byte[] data, int index, int length) throws IndexOutOfBoundsException {
int result = 0;
for (int n = index; n < index + length; n++) {
result <<= 8;
result |= data[n] & 0xFF;
}
return result;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("deviceTypeId", deviceTypeId)
.append("serialNumber", serialNumber).toString();
}
}

View File

@@ -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.homematic.internal.handler;
/**
* Exception if the HomematicGateway is not available.
*
* @author Gerhard Riegler - Initial contribution
*/
public class GatewayNotAvailableException extends Exception {
private static final long serialVersionUID = 95628391238530L;
public GatewayNotAvailableException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,441 @@
/**
* 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.homematic.internal.handler;
import static org.openhab.binding.homematic.internal.HomematicBindingConstants.CHANNEL_TYPE_DUTY_CYCLE_RATIO;
import static org.openhab.core.thing.Thing.*;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.HomematicGateway;
import org.openhab.binding.homematic.internal.communicator.HomematicGatewayAdapter;
import org.openhab.binding.homematic.internal.communicator.HomematicGatewayFactory;
import org.openhab.binding.homematic.internal.discovery.HomematicDeviceDiscoveryService;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmGatewayInfo;
import org.openhab.binding.homematic.internal.type.HomematicTypeGenerator;
import org.openhab.binding.homematic.internal.type.UidUtils;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link HomematicBridgeHandler} is the handler for a Homematic gateway and connects it to the framework.
*
* @author Gerhard Riegler - Initial contribution
*/
public class HomematicBridgeHandler extends BaseBridgeHandler implements HomematicGatewayAdapter {
private final Logger logger = LoggerFactory.getLogger(HomematicBridgeHandler.class);
private static final long REINITIALIZE_DELAY_SECONDS = 10;
private static final int DUTY_CYCLE_RATIO_LIMIT = 99;
private static final int DUTY_CYCLE_DISCONNECTED = -1;
private static SimplePortPool portPool = new SimplePortPool();
private final Object dutyCycleRatioUpdateLock = new Object();
private final Object initDisposeLock = new Object();
private Future<?> initializeFuture;
private boolean isDisposed;
private HomematicConfig config;
private HomematicGateway gateway;
private final HomematicTypeGenerator typeGenerator;
private final HttpClient httpClient;
private HomematicDeviceDiscoveryService discoveryService;
private final String ipv4Address;
private boolean isInDutyCycle = false;
private int dutyCycleRatio = 0;
public HomematicBridgeHandler(@NonNull Bridge bridge, HomematicTypeGenerator typeGenerator, String ipv4Address,
HttpClient httpClient) {
super(bridge);
this.typeGenerator = typeGenerator;
this.ipv4Address = ipv4Address;
this.httpClient = httpClient;
}
@Override
public void initialize() {
synchronized (initDisposeLock) {
isDisposed = false;
initializeFuture = scheduler.submit(this::initializeInternal);
}
}
public void setDiscoveryService(HomematicDeviceDiscoveryService discoveryService) {
this.discoveryService = discoveryService;
}
private void initializeInternal() {
synchronized (initDisposeLock) {
config = createHomematicConfig();
try {
String id = getThing().getUID().getId();
gateway = HomematicGatewayFactory.createGateway(id, config, this, httpClient);
configureThingProperties();
gateway.initialize();
// scan for already known devices (new devices will not be discovered,
// since installMode==true is only achieved if the bridge is online
discoveryService.startScan(null);
discoveryService.waitForScanFinishing();
updateStatus(ThingStatus.ONLINE);
if (!config.getGatewayInfo().isHomegear()) {
try {
gateway.loadRssiValues();
} catch (IOException ex) {
logger.warn("Unable to load RSSI values from bridge '{}'", getThing().getUID().getId());
logger.error("{}", ex.getMessage(), ex);
}
}
gateway.startWatchdogs();
} catch (IOException ex) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage());
logger.debug(
"Homematic bridge was set to OFFLINE-COMMUNICATION_ERROR due to the following exception: {}",
ex.getMessage(), ex);
disposeInternal();
scheduleReinitialize();
}
}
}
private void configureThingProperties() {
final HmGatewayInfo info = config.getGatewayInfo();
final Map<String, String> properties = getThing().getProperties();
if (!properties.containsKey(PROPERTY_FIRMWARE_VERSION)) {
getThing().setProperty(PROPERTY_FIRMWARE_VERSION, info.getFirmware());
}
if (!properties.containsKey(PROPERTY_SERIAL_NUMBER)) {
getThing().setProperty(PROPERTY_SERIAL_NUMBER, info.getAddress());
}
if (!properties.containsKey(PROPERTY_MODEL_ID)) {
getThing().setProperty(PROPERTY_MODEL_ID, info.getType());
}
}
/**
* Schedules a reinitialization, if the Homematic gateway is not reachable at bridge startup.
*/
private void scheduleReinitialize() {
if (!isDisposed) {
initializeFuture = scheduler.schedule(this::initializeInternal, REINITIALIZE_DELAY_SECONDS,
TimeUnit.SECONDS);
}
}
@Override
public void dispose() {
synchronized (initDisposeLock) {
super.dispose();
if (initializeFuture != null) {
initializeFuture.cancel(true);
}
disposeInternal();
isDisposed = true;
}
}
private void disposeInternal() {
logger.debug("Disposing bridge '{}'", getThing().getUID().getId());
if (discoveryService != null) {
discoveryService.stopScan();
}
if (gateway != null) {
gateway.dispose();
}
if (config != null) {
portPool.release(config.getXmlCallbackPort());
portPool.release(config.getBinCallbackPort());
}
}
/**
* Sets the OFFLINE status for all things of this bridge that has been removed from the gateway.
*/
@SuppressWarnings("null")
public void setOfflineStatus() {
for (Thing hmThing : getThing().getThings()) {
try {
gateway.getDevice(UidUtils.getHomematicAddress(hmThing));
} catch (HomematicClientException e) {
if (hmThing.getHandler() != null) {
((HomematicThingHandler) hmThing.getHandler()).handleRemoval();
}
}
}
}
/**
* Creates the configuration for the HomematicGateway.
*/
private HomematicConfig createHomematicConfig() {
HomematicConfig homematicConfig = getThing().getConfiguration().as(HomematicConfig.class);
if (homematicConfig.getCallbackHost() == null) {
homematicConfig.setCallbackHost(this.ipv4Address);
}
if (homematicConfig.getBindAddress() == null) {
homematicConfig.setBindAddress(homematicConfig.getCallbackHost());
}
if (homematicConfig.getXmlCallbackPort() == 0) {
homematicConfig.setXmlCallbackPort(portPool.getNextPort());
} else {
portPool.setInUse(homematicConfig.getXmlCallbackPort());
}
if (homematicConfig.getBinCallbackPort() == 0) {
homematicConfig.setBinCallbackPort(portPool.getNextPort());
} else {
portPool.setInUse(homematicConfig.getBinCallbackPort());
}
logger.debug("{}", homematicConfig);
return homematicConfig;
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(HomematicDeviceDiscoveryService.class);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (RefreshType.REFRESH == command) {
logger.debug("Refreshing bridge '{}'", getThing().getUID().getId());
reloadAllDeviceValues();
}
}
/**
* Returns the TypeGenerator.
*/
public HomematicTypeGenerator getTypeGenerator() {
return typeGenerator;
}
/**
* Returns the HomematicGateway.
*/
public HomematicGateway getGateway() {
return gateway;
}
/**
* Updates the thing for the given Homematic device.
*/
private void updateThing(HmDevice device) {
Thing hmThing = getThing().getThing(UidUtils.generateThingUID(device, getThing()));
if (hmThing != null) {
HomematicThingHandler thingHandler = (HomematicThingHandler) hmThing.getHandler();
if (thingHandler != null) {
thingHandler.thingUpdated(hmThing);
for (Channel channel : hmThing.getChannels()) {
thingHandler.handleRefresh(channel.getUID());
}
}
}
}
@Override
public void onStateUpdated(HmDatapoint dp) {
Thing hmThing = getThing().getThing(UidUtils.generateThingUID(dp.getChannel().getDevice(), getThing()));
if (hmThing != null) {
final ThingStatus status = hmThing.getStatus();
if (status == ThingStatus.ONLINE || status == ThingStatus.OFFLINE) {
HomematicThingHandler thingHandler = (HomematicThingHandler) hmThing.getHandler();
if (thingHandler != null) {
thingHandler.updateDatapointState(dp);
}
}
}
}
@Override
public HmDatapointConfig getDatapointConfig(HmDatapoint dp) {
Thing hmThing = getThing().getThing(UidUtils.generateThingUID(dp.getChannel().getDevice(), getThing()));
if (hmThing != null) {
HomematicThingHandler thingHandler = (HomematicThingHandler) hmThing.getHandler();
if (thingHandler != null) {
return thingHandler.getChannelConfig(dp);
}
}
return new HmDatapointConfig();
}
@Override
public void onNewDevice(HmDevice device) {
onDeviceLoaded(device);
updateThing(device);
}
@SuppressWarnings("null")
@Override
public void onDeviceDeleted(HmDevice device) {
discoveryService.deviceRemoved(device);
updateThing(device);
Thing hmThing = getThing().getThing(UidUtils.generateThingUID(device, getThing()));
if (hmThing != null && hmThing.getHandler() != null) {
((HomematicThingHandler) hmThing.getHandler()).deviceRemoved();
}
}
@Override
public void onConnectionLost() {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection lost");
}
@Override
public void onConnectionResumed() {
updateStatus(ThingStatus.ONLINE);
reloadAllDeviceValues();
}
@Override
public void onDeviceLoaded(HmDevice device) {
typeGenerator.generate(device);
if (discoveryService != null) {
discoveryService.deviceDiscovered(device);
}
Thing hmThing = getThing().getThing(UidUtils.generateThingUID(device, getThing()));
if (hmThing != null) {
HomematicThingHandler thingHandler = (HomematicThingHandler) hmThing.getHandler();
if (thingHandler != null) {
thingHandler.deviceLoaded(device);
}
}
}
@Override
public void onDutyCycleRatioUpdate(int dutyCycleRatio) {
synchronized (dutyCycleRatioUpdateLock) {
this.dutyCycleRatio = dutyCycleRatio;
Channel dutyCycleRatioChannel = thing.getChannel(CHANNEL_TYPE_DUTY_CYCLE_RATIO);
if (dutyCycleRatioChannel != null) {
this.updateState(dutyCycleRatioChannel.getUID(),
new DecimalType(dutyCycleRatio < 0 ? 0 : dutyCycleRatio));
}
if (!isInDutyCycle) {
if (dutyCycleRatio >= DUTY_CYCLE_RATIO_LIMIT) {
logger.info("Duty cycle threshold exceeded by homematic bridge {}, it will go OFFLINE.",
thing.getUID());
isInDutyCycle = true;
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DUTY_CYCLE);
} else if (dutyCycleRatio == DUTY_CYCLE_DISCONNECTED) {
logger.info(
"Duty cycle indicates a communication problem by homematic bridge {}, it will go OFFLINE.",
thing.getUID());
isInDutyCycle = true;
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
} else {
if (dutyCycleRatio < DUTY_CYCLE_RATIO_LIMIT && dutyCycleRatio != DUTY_CYCLE_DISCONNECTED) {
logger.info("Homematic bridge {}: duty cycle back to normal and bridge will come ONLINE again.",
thing.getUID());
isInDutyCycle = false;
this.updateStatus(ThingStatus.ONLINE);
}
}
}
}
/**
* Returns the last value for the duty cycle ratio that was retrieved from the homematic gateway.
*
* @return The duty cycle ratio of the gateway
*/
public int getDutyCycleRatio() {
return dutyCycleRatio;
}
@Override
public void reloadDeviceValues(HmDevice device) {
updateThing(device);
if (device.isGatewayExtras()) {
typeGenerator.generate(device);
}
}
@Override
public void reloadAllDeviceValues() {
for (Thing hmThing : getThing().getThings()) {
try {
HmDevice device = gateway.getDevice(UidUtils.getHomematicAddress(hmThing));
gateway.triggerDeviceValuesReload(device);
} catch (HomematicClientException ex) {
logger.warn("{}", ex.getMessage());
}
}
}
@Override
public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
if (((HomematicThingHandler) childHandler).isDeletionPending()) {
deleteFromGateway(UidUtils.getHomematicAddress(childThing), false, true, false);
}
}
/**
* Updates the {@link HmDatapoint} by reloading the value from the homematic gateway.
*
* @param dp The HmDatapoint that shall be updated
* @throws IOException If there is a problem while communicating to the gateway
*/
public void updateDatapoint(HmDatapoint dp) throws IOException {
getGateway().loadDatapointValue(dp);
}
/**
* Deletes a device from the gateway.
*
* @param address The address of the device to be deleted
* @param reset <i>true</i> will perform a factory reset on the device before deleting it.
* @param force <i>true</i> will delete the device even if it is not reachable.
* @param defer <i>true</i> will delete the device once it becomes available.
*/
public void deleteFromGateway(String address, boolean reset, boolean force, boolean defer) {
scheduler.submit(() -> {
logger.debug("Deleting the device '{}' from gateway '{}'", address, getBridge());
getGateway().deleteDevice(address, reset, force, defer);
});
}
}

View File

@@ -0,0 +1,592 @@
/**
* 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.homematic.internal.handler;
import static org.openhab.binding.homematic.internal.HomematicBindingConstants.*;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Future;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.openhab.binding.homematic.internal.HomematicBindingConstants;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.HomematicGateway;
import org.openhab.binding.homematic.internal.converter.ConverterException;
import org.openhab.binding.homematic.internal.converter.ConverterFactory;
import org.openhab.binding.homematic.internal.converter.ConverterTypeException;
import org.openhab.binding.homematic.internal.converter.TypeConverter;
import org.openhab.binding.homematic.internal.misc.HomematicClientException;
import org.openhab.binding.homematic.internal.misc.HomematicConstants;
import org.openhab.binding.homematic.internal.model.HmChannel;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.openhab.binding.homematic.internal.model.HmDevice;
import org.openhab.binding.homematic.internal.model.HmParamsetType;
import org.openhab.binding.homematic.internal.type.HomematicTypeGeneratorImpl;
import org.openhab.binding.homematic.internal.type.MetadataUtils;
import org.openhab.binding.homematic.internal.type.UidUtils;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.core.validation.ConfigValidationException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HomematicThingHandler} is responsible for handling commands, which are sent to one of the channels.
*
* @author Gerhard Riegler - Initial contribution
*/
public class HomematicThingHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(HomematicThingHandler.class);
private Future<?> initFuture;
private final Object initLock = new Object();
private volatile boolean deviceDeletionPending = false;
public HomematicThingHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
if (initFuture != null) {
return;
}
initFuture = scheduler.submit(() -> {
initFuture = null;
try {
synchronized (initLock) {
doInitializeInBackground();
}
} catch (HomematicClientException ex) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
} catch (IOException ex) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage());
} catch (GatewayNotAvailableException ex) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, ex.getMessage());
} catch (Exception ex) {
logger.error("{}", ex.getMessage(), ex);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, ex.getMessage());
}
});
}
private void doInitializeInBackground() throws GatewayNotAvailableException, HomematicClientException, IOException {
HomematicGateway gateway = getHomematicGateway();
HmDevice device = gateway.getDevice(UidUtils.getHomematicAddress(getThing()));
HmChannel channelZero = device.getChannel(0);
loadHomematicChannelValues(channelZero);
updateStatus(device);
logger.debug("Initializing thing '{}' from gateway '{}'", getThing().getUID(), gateway.getId());
// update properties
Map<String, String> properties = editProperties();
setProperty(properties, channelZero, PROPERTY_BATTERY_TYPE, VIRTUAL_DATAPOINT_NAME_BATTERY_TYPE);
setProperty(properties, channelZero, Thing.PROPERTY_FIRMWARE_VERSION, VIRTUAL_DATAPOINT_NAME_FIRMWARE);
setProperty(properties, channelZero, Thing.PROPERTY_SERIAL_NUMBER, device.getAddress());
setProperty(properties, channelZero, PROPERTY_AES_KEY, DATAPOINT_NAME_AES_KEY);
updateProperties(properties);
// update data point list for reconfigurable channels
for (HmChannel channel : device.getChannels()) {
if (channel.isReconfigurable()) {
loadHomematicChannelValues(channel);
if (channel.checkForChannelFunctionChange()) {
gateway.updateChannelValueDatapoints(channel);
}
}
}
// update configurations
Configuration config = editConfiguration();
for (HmChannel channel : device.getChannels()) {
loadHomematicChannelValues(channel);
for (HmDatapoint dp : channel.getDatapoints()) {
if (dp.getParamsetType() == HmParamsetType.MASTER) {
config.put(MetadataUtils.getParameterName(dp),
dp.isEnumType() ? dp.getOptionValue() : dp.getValue());
}
}
}
updateConfiguration(config);
// update thing channel list for reconfigurable channels (relies on the new value of the
// CHANNEL_FUNCTION datapoint fetched during configuration update)
List<Channel> thingChannels = new ArrayList<>(getThing().getChannels());
if (updateDynamicChannelList(device, thingChannels)) {
updateThing(editThing().withChannels(thingChannels).build());
}
}
/**
* Update the given thing channel list to reflect the device's current datapoint set
*
* @return true if the list was modified, false if it was not modified
*/
private boolean updateDynamicChannelList(HmDevice device, List<Channel> thingChannels) {
boolean changed = false;
for (HmChannel channel : device.getChannels()) {
if (!channel.isReconfigurable()) {
continue;
}
final String expectedFunction = channel
.getDatapoint(HmParamsetType.MASTER, HomematicConstants.DATAPOINT_NAME_CHANNEL_FUNCTION)
.getOptionValue();
final String propertyName = String.format(PROPERTY_DYNAMIC_FUNCTION_FORMAT, channel.getNumber());
// remove thing channels that were configured for a different function
Iterator<Channel> channelIter = thingChannels.iterator();
while (channelIter.hasNext()) {
Map<String, String> properties = channelIter.next().getProperties();
String function = properties.get(propertyName);
if (function != null && !function.equals(expectedFunction)) {
channelIter.remove();
changed = true;
}
}
for (HmDatapoint dp : channel.getDatapoints()) {
if (HomematicTypeGeneratorImpl.isIgnoredDatapoint(dp)
|| dp.getParamsetType() != HmParamsetType.VALUES) {
continue;
}
ChannelUID channelUID = UidUtils.generateChannelUID(dp, getThing().getUID());
if (containsChannel(thingChannels, channelUID)) {
// Channel is already present -> channel configuration likely hasn't changed
continue;
}
Map<String, String> channelProps = new HashMap<>();
channelProps.put(propertyName, expectedFunction);
Channel thingChannel = ChannelBuilder.create(channelUID, MetadataUtils.getItemType(dp))
.withProperties(channelProps).withLabel(MetadataUtils.getLabel(dp))
.withDescription(MetadataUtils.getDatapointDescription(dp))
.withType(UidUtils.generateChannelTypeUID(dp)).build();
thingChannels.add(thingChannel);
changed = true;
}
}
return changed;
}
/**
* Checks whether the given list includes a channel with the given UID
*/
private static boolean containsChannel(List<Channel> channels, ChannelUID channelUID) {
for (Channel channel : channels) {
ChannelUID uid = channel.getUID();
if (StringUtils.equals(channelUID.getGroupId(), uid.getGroupId())
&& StringUtils.equals(channelUID.getId(), uid.getId())) {
return true;
}
}
return false;
}
/**
* Sets a thing property with a datapoint value.
*/
private void setProperty(Map<String, String> properties, HmChannel channelZero, String propertyName,
String datapointName) {
HmDatapoint dp = channelZero
.getDatapoint(new HmDatapointInfo(HmParamsetType.VALUES, channelZero, datapointName));
if (dp != null) {
properties.put(propertyName, ObjectUtils.toString(dp.getValue()));
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
handleRefresh(channelUID);
}
/**
* Updates the state of the given channel.
*/
protected void handleRefresh(ChannelUID channelUID) {
try {
if (thing.getStatus() == ThingStatus.ONLINE) {
logger.debug("Updating channel '{}' from thing id '{}'", channelUID, getThing().getUID().getId());
updateChannelState(channelUID);
}
} catch (Exception ex) {
logger.warn("{}", ex.getMessage());
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Received command '{}' for channel '{}'", command, channelUID);
HmDatapoint dp = null;
try {
HomematicGateway gateway = getHomematicGateway();
HmDatapointInfo dpInfo = UidUtils.createHmDatapointInfo(channelUID);
if (RefreshType.REFRESH == command) {
logger.debug("Refreshing {}", dpInfo);
dpInfo = new HmDatapointInfo(dpInfo.getAddress(), HmParamsetType.VALUES, 0,
VIRTUAL_DATAPOINT_NAME_RELOAD_FROM_GATEWAY);
dp = gateway.getDatapoint(dpInfo);
sendDatapoint(dp, new HmDatapointConfig(), Boolean.TRUE);
} else {
Channel channel = getThing().getChannel(channelUID.getId());
if (channel == null) {
logger.warn("Channel '{}' not found in thing '{}' on gateway '{}'", channelUID, getThing().getUID(),
gateway.getId());
} else {
if (StopMoveType.STOP == command && DATAPOINT_NAME_LEVEL.equals(dpInfo.getName())) {
// special case with stop type (rollershutter)
dpInfo.setName(DATAPOINT_NAME_STOP);
HmDatapoint stopDp = gateway.getDatapoint(dpInfo);
ChannelUID stopChannelUID = UidUtils.generateChannelUID(stopDp, getThing().getUID());
handleCommand(stopChannelUID, OnOffType.ON);
} else {
dp = gateway.getDatapoint(dpInfo);
TypeConverter<?> converter = ConverterFactory.createConverter(channel.getAcceptedItemType());
Object newValue = converter.convertToBinding(command, dp);
HmDatapointConfig config = getChannelConfig(channel, dp);
sendDatapoint(dp, config, newValue);
}
}
}
} catch (HomematicClientException | GatewayNotAvailableException ex) {
logger.warn("{}", ex.getMessage());
} catch (IOException ex) {
if (dp != null && dp.getChannel().getDevice().isOffline()) {
logger.warn("Device '{}' is OFFLINE, can't send command '{}' for channel '{}'",
dp.getChannel().getDevice().getAddress(), command, channelUID);
logger.trace("{}", ex.getMessage(), ex);
} else {
logger.error("{}", ex.getMessage(), ex);
}
} catch (ConverterTypeException ex) {
logger.warn("{}, please check the item type and the commands in your scripts", ex.getMessage());
} catch (Exception ex) {
logger.error("{}", ex.getMessage(), ex);
}
}
private void sendDatapoint(HmDatapoint dp, HmDatapointConfig config, Object newValue)
throws IOException, HomematicClientException, GatewayNotAvailableException {
String rxMode = getRxModeForDatapointTransmission(dp.getName(), dp.getValue(), newValue);
getHomematicGateway().sendDatapoint(dp, config, newValue, rxMode);
}
/**
* Returns the rx mode that shall be used for transmitting a new value of a datapoint to the device. The
* HomematicThingHandler always uses the default rx mode; custom thing handlers can override this method to
* adjust the rx mode.
*
* @param datapointName The datapoint that will be updated on the device
* @param currentValue The current value of the datapoint
* @param newValue The value that will be sent to the device
* @return The rxMode ({@link HomematicBindingConstants#RX_BURST_MODE "BURST"} for burst mode,
* {@link HomematicBindingConstants#RX_WAKEUP_MODE "WAKEUP"} for wakeup mode, or null for the default mode)
*/
protected String getRxModeForDatapointTransmission(String datapointName, Object currentValue, Object newValue) {
return null;
}
/**
* Evaluates the channel and datapoint for this channelUID and updates the state of the channel.
*/
private void updateChannelState(ChannelUID channelUID)
throws GatewayNotAvailableException, HomematicClientException, IOException, ConverterException {
HomematicGateway gateway = getHomematicGateway();
HmDatapointInfo dpInfo = UidUtils.createHmDatapointInfo(channelUID);
HmDatapoint dp = gateway.getDatapoint(dpInfo);
Channel channel = getThing().getChannel(channelUID.getId());
updateChannelState(dp, channel);
}
/**
* Sets the configuration or evaluates the channel for this datapoint and updates the state of the channel.
*/
protected void updateDatapointState(HmDatapoint dp) {
try {
updateStatus(dp.getChannel().getDevice());
if (dp.getParamsetType() == HmParamsetType.MASTER) {
// update configuration
Configuration config = editConfiguration();
config.put(MetadataUtils.getParameterName(dp), dp.isEnumType() ? dp.getOptionValue() : dp.getValue());
updateConfiguration(config);
} else if (!HomematicTypeGeneratorImpl.isIgnoredDatapoint(dp)) {
// update channel
ChannelUID channelUID = UidUtils.generateChannelUID(dp, thing.getUID());
Channel channel = thing.getChannel(channelUID.getId());
if (channel != null) {
updateChannelState(dp, channel);
} else {
logger.warn("Channel not found for datapoint '{}'", new HmDatapointInfo(dp));
}
}
} catch (GatewayNotAvailableException ex) {
// ignore
} catch (Exception ex) {
logger.error("{}", ex.getMessage(), ex);
}
}
/**
* Converts the value of the datapoint to a State, updates the channel and also sets the thing status if necessary.
*/
private void updateChannelState(final HmDatapoint dp, Channel channel)
throws IOException, GatewayNotAvailableException, ConverterException {
if (dp.isTrigger()) {
if (dp.getValue() != null) {
triggerChannel(channel.getUID(), ObjectUtils.toString(dp.getValue()));
}
} else if (isLinked(channel)) {
loadHomematicChannelValues(dp.getChannel());
TypeConverter<?> converter = ConverterFactory.createConverter(channel.getAcceptedItemType());
State state = converter.convertFromBinding(dp);
if (state != null) {
updateState(channel.getUID(), state);
} else {
logger.debug("Failed to get converted state from datapoint '{}'", dp.getName());
}
}
}
/**
* Loads all values for the given Homematic channel if it is not initialized.
*/
private void loadHomematicChannelValues(HmChannel hmChannel) throws GatewayNotAvailableException, IOException {
if (!hmChannel.isInitialized()) {
synchronized (this) {
if (!hmChannel.isInitialized()) {
try {
getHomematicGateway().loadChannelValues(hmChannel);
} catch (IOException ex) {
if (hmChannel.getDevice().isOffline()) {
logger.warn("Device '{}' is OFFLINE, can't update channel '{}'",
hmChannel.getDevice().getAddress(), hmChannel.getNumber());
} else {
throw ex;
}
}
}
}
}
}
/**
* Updates the thing status based on device status.
*/
private void updateStatus(HmDevice device) throws GatewayNotAvailableException, IOException {
loadHomematicChannelValues(device.getChannel(0));
ThingStatus oldStatus = thing.getStatus();
ThingStatus newStatus = ThingStatus.ONLINE;
ThingStatusDetail newDetail = ThingStatusDetail.NONE;
if ((getBridge() != null) && (getBridge().getStatus() == ThingStatus.OFFLINE)) {
newStatus = ThingStatus.OFFLINE;
newDetail = ThingStatusDetail.BRIDGE_OFFLINE;
} else if (device.isFirmwareUpdating()) {
newStatus = ThingStatus.OFFLINE;
newDetail = ThingStatusDetail.FIRMWARE_UPDATING;
} else if (device.isUnreach()) {
newStatus = ThingStatus.OFFLINE;
newDetail = ThingStatusDetail.COMMUNICATION_ERROR;
} else if (device.isConfigPending() || device.isUpdatePending()) {
newDetail = ThingStatusDetail.CONFIGURATION_PENDING;
}
if (thing.getStatus() != newStatus || thing.getStatusInfo().getStatusDetail() != newDetail) {
updateStatus(newStatus, newDetail);
}
if (oldStatus == ThingStatus.OFFLINE && newStatus == ThingStatus.ONLINE) {
initialize();
}
}
/**
* Returns true, if the channel is linked at least to one item.
*/
private boolean isLinked(Channel channel) {
return channel != null && super.isLinked(channel.getUID().getId());
}
/**
* Returns the channel config for the given datapoint.
*/
protected HmDatapointConfig getChannelConfig(HmDatapoint dp) {
ChannelUID channelUid = UidUtils.generateChannelUID(dp, getThing().getUID());
Channel channel = getThing().getChannel(channelUid.getId());
return channel != null ? getChannelConfig(channel, dp) : new HmDatapointConfig();
}
/**
* Returns the config for a channel.
*/
private HmDatapointConfig getChannelConfig(Channel channel, HmDatapoint dp) {
return channel.getConfiguration().as(HmDatapointConfig.class);
}
/**
* Returns the Homematic gateway if the bridge is available.
*/
private HomematicGateway getHomematicGateway() throws GatewayNotAvailableException {
final Bridge bridge = getBridge();
if (bridge != null) {
HomematicBridgeHandler bridgeHandler = (HomematicBridgeHandler) bridge.getHandler();
if (bridgeHandler != null && bridgeHandler.getGateway() != null) {
return bridgeHandler.getGateway();
}
}
throw new GatewayNotAvailableException("HomematicGateway not yet available!");
}
@Override
public void handleConfigurationUpdate(Map<String, Object> configurationParameters)
throws ConfigValidationException {
super.handleConfigurationUpdate(configurationParameters);
try {
HomematicGateway gateway = getHomematicGateway();
HmDevice device = gateway.getDevice(UidUtils.getHomematicAddress(getThing()));
for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
String key = configurationParameter.getKey();
Object newValue = configurationParameter.getValue();
if (key.startsWith("HMP_")) {
key = StringUtils.removeStart(key, "HMP_");
Integer channelNumber = NumberUtils.toInt(StringUtils.substringBefore(key, "_"));
String dpName = StringUtils.substringAfter(key, "_");
HmDatapointInfo dpInfo = new HmDatapointInfo(device.getAddress(), HmParamsetType.MASTER,
channelNumber, dpName);
HmDatapoint dp = device.getChannel(channelNumber).getDatapoint(dpInfo);
if (dp != null) {
try {
if (newValue != null) {
if (newValue instanceof BigDecimal) {
final BigDecimal decimal = (BigDecimal) newValue;
if (dp.isIntegerType()) {
newValue = decimal.intValue();
} else if (dp.isFloatType()) {
newValue = decimal.doubleValue();
}
}
if (ObjectUtils.notEqual(dp.isEnumType() ? dp.getOptionValue() : dp.getValue(),
newValue)) {
sendDatapoint(dp, new HmDatapointConfig(), newValue);
}
}
} catch (IOException ex) {
logger.error("Error setting thing property {}: {}", dpInfo, ex.getMessage());
}
} else {
logger.error("Can't find datapoint for thing property {}", dpInfo);
}
}
}
gateway.triggerDeviceValuesReload(device);
} catch (HomematicClientException | GatewayNotAvailableException ex) {
logger.error("Error setting thing properties: {}", ex.getMessage(), ex);
}
}
@SuppressWarnings("null")
@Override
public synchronized void handleRemoval() {
final Bridge bridge;
final ThingHandler handler;
if ((bridge = getBridge()) == null || (handler = bridge.getHandler()) == null) {
super.handleRemoval();
return;
}
final HomematicConfig config = bridge.getConfiguration().as(HomematicConfig.class);
final boolean factoryResetOnDeletion = config.isFactoryResetOnDeletion();
final boolean unpairOnDeletion = factoryResetOnDeletion || config.isUnpairOnDeletion();
if (unpairOnDeletion) {
deviceDeletionPending = true;
((HomematicBridgeHandler) handler).deleteFromGateway(UidUtils.getHomematicAddress(thing),
factoryResetOnDeletion, false, true);
} else {
super.handleRemoval();
}
}
/**
* Called by the bridgeHandler when this device has been removed from the gateway.
*/
public synchronized void deviceRemoved() {
deviceDeletionPending = false;
if (getThing().getStatus() == ThingStatus.REMOVING) {
// thing removal was initiated on ESH side
updateStatus(ThingStatus.REMOVED);
} else {
// device removal was initiated on homematic side, thing is not removed
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE);
}
}
/**
* Called by the bridgeHandler when the device for this thing has been added to the gateway.
* This is used to reconnect a device that was previously unpaired.
*
* @param device The device that has been added to the gateway
*/
public void deviceLoaded(HmDevice device) {
try {
updateStatus(device);
} catch (GatewayNotAvailableException ex) {
// ignore
} catch (IOException ex) {
logger.warn("Could not reinitialize the device '{}': {}", device.getAddress(), ex.getMessage(), ex);
}
}
/**
* Returns whether the device deletion is pending.
*
* @return true, if the deletion of this device on its gateway has been triggered but has not yet completed
*/
public synchronized boolean isDeletionPending() {
return deviceDeletionPending;
}
}

View File

@@ -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.homematic.internal.handler;
import static org.openhab.binding.homematic.internal.HomematicBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.homematic.internal.type.HomematicTypeGenerator;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link HomematicThingHandlerFactory} is responsible for creating thing and bridge handlers.
*
* @author Gerhard Riegler - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.homematic")
@NonNullByDefault
public class HomematicThingHandlerFactory extends BaseThingHandlerFactory {
private final HomematicTypeGenerator typeGenerator;
private final NetworkAddressService networkAddressService;
private final HttpClient httpClient;
@Activate
public HomematicThingHandlerFactory(@Reference HomematicTypeGenerator typeGenerator,
@Reference NetworkAddressService networkAddressService, @Reference HttpClientFactory httpClientFactory) {
this.typeGenerator = typeGenerator;
this.networkAddressService = networkAddressService;
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return BINDING_ID.equals(thingTypeUID.getBindingId());
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
if (THING_TYPE_BRIDGE.equals(thing.getThingTypeUID())) {
return new HomematicBridgeHandler((Bridge) thing, typeGenerator,
networkAddressService.getPrimaryIpv4HostAddress(), httpClient);
} else {
return new HomematicThingHandler(thing);
}
}
}

View File

@@ -0,0 +1,87 @@
/**
* 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.homematic.internal.handler;
import java.util.ArrayList;
import java.util.List;
/**
* A very simple pool implementation that handles free port numbers used by the RPC server if multiple bridges are
* configured.
*
* @author Gerhard Riegler - Initial contribution
*/
public class SimplePortPool {
private static int START_PORT = 9125;
private List<PortInfo> availablePorts = new ArrayList<>();
/**
* Adds the specified port to the pool an mark it as in use.
*/
public void setInUse(int port) {
PortInfo portInfo = new PortInfo();
portInfo.port = port;
portInfo.free = false;
availablePorts.add(portInfo);
}
/**
* Returns the next free port number.
*/
public synchronized int getNextPort() {
for (PortInfo portInfo : availablePorts) {
if (portInfo.free) {
portInfo.free = false;
return portInfo.port;
}
}
PortInfo portInfo = new PortInfo();
while (isPortInUse(START_PORT++)) {
}
portInfo.port = START_PORT - 1;
portInfo.free = false;
availablePorts.add(portInfo);
return portInfo.port;
}
/**
* Returns true, if the specified port is not in use.
*/
private boolean isPortInUse(int port) {
for (PortInfo portInfo : availablePorts) {
if (portInfo.port == port) {
return !portInfo.free;
}
}
return false;
}
/**
* Releases a unused port number.
*/
public synchronized void release(int port) {
for (PortInfo portInfo : availablePorts) {
if (portInfo.port == port) {
portInfo.free = true;
}
}
}
private class PortInfo {
int port;
boolean free;
}
}

View File

@@ -0,0 +1,88 @@
/**
* 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.homematic.internal.misc;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Executes a callback method either immediately or after a given delay for a datapoint.
*
* @author Gerhard Riegler - Initial contribution
*/
public class DelayedExecuter {
private final Logger logger = LoggerFactory.getLogger(DelayedExecuter.class);
private Map<HmDatapointInfo, Timer> delayedEvents = new HashMap<>();
/**
* Executes a callback method either immediately or after a given delay.
*/
public void start(final HmDatapointInfo dpInfo, final double delay, final DelayedExecuterCallback callback)
throws IOException, HomematicClientException {
if (delay > 0.0) {
synchronized (DelayedExecuter.class) {
logger.debug("Delaying event for {} seconds: '{}'", delay, dpInfo);
Timer timer = delayedEvents.get(dpInfo);
if (timer != null) {
timer.cancel();
}
timer = new Timer();
delayedEvents.put(dpInfo, timer);
timer.schedule(new TimerTask() {
@Override
public void run() {
logger.debug("Executing delayed event for '{}'", dpInfo);
delayedEvents.remove(dpInfo);
try {
callback.execute();
} catch (Exception ex) {
logger.error("{}", ex.getMessage(), ex);
}
}
}, (long) (delay * 1000));
}
} else {
callback.execute();
}
}
/**
* Stops all delayed events.
*/
public void stop() {
for (Timer timer : delayedEvents.values()) {
timer.cancel();
}
delayedEvents.clear();
}
/**
* Callback interface for the {@link DelayedExecuter}.
*
* @author Gerhard Riegler - Initial contribution
*/
public interface DelayedExecuterCallback {
public void execute() throws IOException, HomematicClientException;
}
}

Some files were not shown because too many files have changed in this diff Show More