added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
32
bundles/org.openhab.binding.lcn/.classpath
Normal file
32
bundles/org.openhab.binding.lcn/.classpath
Normal 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>
|
||||
23
bundles/org.openhab.binding.lcn/.project
Normal file
23
bundles/org.openhab.binding.lcn/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.lcn</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>
|
||||
13
bundles/org.openhab.binding.lcn/NOTICE
Normal file
13
bundles/org.openhab.binding.lcn/NOTICE
Normal file
@@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
||||
570
bundles/org.openhab.binding.lcn/README.md
Normal file
570
bundles/org.openhab.binding.lcn/README.md
Normal file
@@ -0,0 +1,570 @@
|
||||
# LCN Binding
|
||||
|
||||
[Local Control Network (LCN)](http://www.lcn.eu) is a building automation system for small and very large installations.
|
||||
It is capable of controlling lights, shutters, access control etc. and can process data from several sensor types.
|
||||
It has been introduced in 1992.
|
||||
|
||||
A broad range of glass key panels, displays, remote controls, sensors and in- and outputs exist.
|
||||
The system can handle up to 30,000 bus members, called modules.
|
||||
LCN modules are available for DIN rail and in-wall mounting and feature versatile interfaces. The bus modules and most of the accessories are developed, manufactured and assembled in Germany.
|
||||
|
||||
Bus members are inter-connected via a free wire in the standard NYM cable. Wireless components are available, though.
|
||||
|
||||

|
||||
|
||||
This binding uses TCP/IP to access the LCN bus via the software LCN-PCHK (Windows/Linux) or the DIN rail device LCN-PKE.
|
||||
**This means 1 unused LCN-PCHK license or a LCN-PKE is required**
|
||||
|
||||
## Supported Things
|
||||
|
||||
### Thing: LCN Module
|
||||
|
||||
Any LCN module that should be controlled or visualized, need to be added to openHAB as a *Thing*.
|
||||
|
||||
LCN modules with firmware versions 120612 (2008) and 170602 (2013) were tested with this binding.
|
||||
No known features/changes that need special handling were added until now (2020).
|
||||
Modules with older and newer firmware should work, too.
|
||||
The module hardware types (e.g. LCN-SH, LCN-HU, LCN-UPP, ...) are compatible to each other and can therefore be handled all in the same way.
|
||||
|
||||
Thing Type ID: `module`
|
||||
|
||||
| Name | Description | Type | Required |
|
||||
|-------------|----------------------------------------------------------------|---------|----------|
|
||||
| `moduleId` | The module ID, configured in LCN-PRO | Integer | Yes |
|
||||
| `segmentId` | The segment ID the module is in (0 if no segments are present) | Integer | Yes |
|
||||
|
||||
openHAB's discovery function can be used to add LCN modules automatically.
|
||||
See [Discover LCN Modules](#discover-lcn-modules).
|
||||
|
||||
### Bridge: LCN PCK Gateway
|
||||
|
||||
PCK is the protocol spoken over TCP/IP with a PCK gateway to communicate with the LCN bus.
|
||||
Examples for PCK gateways are the *LCN-PCHK* software running on Windows or Linux and the DIN rail mounting device *LCN-PKE*.
|
||||
|
||||
For each LCN bus, interfaced to openHAB, a PCK gateway needs to be added to openHAB as a *Thing*.
|
||||
|
||||
Several PCK gateways can be added to openHAB to control multiple LCN busses in distinct locations.
|
||||
|
||||
The minimum recommended version is LCN-PCHK 2.8 (older versions will also work, but lack some functionality).
|
||||
Visit [https://www.lcn.eu](https://www.lcn.eu) for updates.
|
||||
|
||||
Thing Type ID: `pckGateway`
|
||||
|
||||
| Name | Description | Type | Required |
|
||||
|-------------|------------------------------------------------------------------------------------------------------------|---------|----------|
|
||||
| `hostname` | Hostname or IP address of the LCN-PCHK gateway | String | Yes |
|
||||
| `port` | TCP port of the LCN-PCHK gateway (default:4114) | Integer | Yes |
|
||||
| `username` | Username configured within LCN-PCHK Monitor | String | Yes |
|
||||
| `password` | Password configured within LCN-PCHK Monitor | String | Yes |
|
||||
| `mode` | Dimmer resolution: `native50` or `native200` See below. | String | Yes |
|
||||
| `timeoutMs` | Period after which an LCN command is resent, when no acknowledge has been received (in ms) (default: 3500) | Integer | Yes |
|
||||
|
||||
> **IMPORTANT:** You need to configure the dimmer output resolution. This setting is valid for the **whole** LCN bus.<br />
|
||||
The setting is either 0-50 steps or 0-200 steps.
|
||||
It **has to be the same** as in the parameterizing software **LCN-PRO** under Options/Settings/Expert Settings.
|
||||
See the following screenshot.
|
||||
|
||||

|
||||
|
||||
When using a wrong dimmer output setting, dimming the outputs will result in unintended behavior.
|
||||
|
||||
### Thing: LCN Group
|
||||
|
||||
LCN modules can be assigned to groups with the programming software *LCN-PRO*.
|
||||
|
||||
To send commands to an LCN group, the group needs to be added to openHAB as a *Thing*.
|
||||
|
||||
One LCN module within the group is used to represent the status of the whole group.
|
||||
For example, when a Dimmer Output is controlled via a LCN group *Thing*, openHAB will always visualize the state of the Dimmer Output of the chosen module. The states of the other modules in the group are ignored for visualization.
|
||||
|
||||
Thing Type ID: `group`
|
||||
|
||||
| Name | Description | Type | Required |
|
||||
|-------------|----------------------------------------------------------------------------------------------------------------------------------------------|---------|----------|
|
||||
| `groupId` | The group number, configured in LCN-PRO | Integer | Yes |
|
||||
| `moduleId` | The module ID of any module in the group. The state of this module is used for visualization of the group as representative for all modules. | Integer | Yes |
|
||||
| `segmentId` | The segment ID of all modules in this group (0 if no segments are present) | Integer | Yes |
|
||||
|
||||
The `groupId` must match the previously configured group number in the programming software *LCN-PRO*.
|
||||
|
||||
## Discovery
|
||||
|
||||
### Discover LCN Modules
|
||||
|
||||
Basic data like the names of all LCN modules in the bus, can be read out by openHAB's discovery function.
|
||||
|
||||
If not all LCN modules get listed on the first run, click on the refresh button to start another scan.
|
||||
|
||||
When adding a module by discovery, the new *Thing*'s UID will be a combination of segment and module id using the following format:
|
||||
*S<segmentId>M<moduleId>* where *segmentId* and *moduleId* are formatted as three-digit numbers with leading zeros.
|
||||
|
||||
### Discover PCK Gateways
|
||||
|
||||
PCK gateways in the LAN can be found automatically by openHAB. This is done by UDP multicast messages on port 4220.
|
||||
The discovery works only if the firewall of the PCK gateway is not configured too strictly.
|
||||
This means on Windows PCs, that the network must be configured as 'private' and not as 'public'.
|
||||
Also, some network switches may block multicast packets.
|
||||
Unfortunately, *LCN-PCHK* listens only on the first network interface of the computer for discovery packets.
|
||||
If your PCK gateway has multiple network interfaces, *LCN-PCHK* may listen on the wrong interface and fails to respond to the discovery request.
|
||||
|
||||
Discovery has successfully been tested with LCN-PCHK 3.2.2 running on a Raspberry Pi with Raspbian and openHAB running on Windows 10.
|
||||
|
||||
If discovery fails, you can add a PCK gateway manually. See [Thing: PCK Gateway](#bridge-lcn-pck-gateway).
|
||||
|
||||
Please be aware that you **have to configure** username, password and the dimmer output resolution also if you use discovery.
|
||||
See [Thing: PCK Gateway](#bridge-lcn-pck-gateway).
|
||||
|
||||
When adding a PCK gateway by discovery, the new *Thing*'s UID is the MAC address of the device, running the PCK gateway.
|
||||
|
||||
## Supported LCN Features and openHAB Channels
|
||||
|
||||
The following table lists all features of LCN and their mappings to openHAB Channels.
|
||||
These Channels are available for the *Thing* LCN module (`module`).
|
||||
LCN group (`group`) has the same Channels, except status-only Channels like binary sensors or transponders.
|
||||
The PCK gateway (`pckGateway`) has no Channels.
|
||||
|
||||
Although, there are many **Not implemented** entries, the vast majority of LCN features can be used with openHAB:<br />
|
||||
If a special command is needed, the [Hit Key](#hit-key) action (German: "Sende Tasten") can be used to hit a module's key virtually and execute an arbitrary command.
|
||||
|
||||
| LCN Feature (English) | LCN Feature (German) | Channel | IDs | Type | Description |
|
||||
|---------------------------------|----------------------------------|------------------------|------|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Dimmer Output Control Single | Ausgang | output | 1-4 | Dimmer, Switch | Sets the dimming value of an output with a given ramp. |
|
||||
| Relay | Relais | relay | 1-8 | Switch | Controls a relay and visualizes its state. |
|
||||
| Visualize Binary Sensor | Binärsensor anzeigen | binarysensor | 1-8 | Contact | Visualizes the state of a binary sensor. |
|
||||
| LED Control | LED-Steuerung | led | 1-12 | Text (ON, OFF, BLINK, FLICKER) | Controls an LED and visualizes its current state. |
|
||||
| Visualize Logic Operations | Logik Funktion anzeigen | logic | 1-4 | Text (NOT, OR, AND) | Visualizes the result of the logic operation. |
|
||||
| Motor/Shutter on Dimmer Outputs | Motor/Rollladen an Ausgängen | rollershutteroutput | 1-4 | Rollershutter | Control roller shutters on dimmer outputs |
|
||||
| Motor/Shutter on Relays | Motor/Rollladen an Relais | rollershutterrelay | 1-4 | Rollershutter | Control roller shutters on relays |
|
||||
| Variables | Variable anzeigen | variable | 1-12 | Number | Sets and visualizes the value of a variable. |
|
||||
| Regulator Set Setpoint | Regler Sollwert ändern | rvarsetpoint | 1-2 | Number | Sets and visualizes the setpoint of a regulator. |
|
||||
| Regulator Lock | Regler sperren | rvarlock | 1-2 | Switch | Locks a regulator and visualizes its locking state. |
|
||||
| Set Thresholds in Register 1 | Schwellwert in Register 1 ändern | thresholdregister1 | 1-4 | Number | Sets and visualizes a threshold in the given threshold register. |
|
||||
| Set Thresholds in Register 2 | Schwellwert in Register 2 ändern | thresholdregister2 | 1-4 | Number | Sets and visualizes a threshold in the given threshold register. |
|
||||
| Set Thresholds in Register 3 | Schwellwert in Register 3 ändern | thresholdregister3 | 1-4 | Number | Sets and visualizes a threshold in the given threshold register. |
|
||||
| Set Thresholds in Register 4 | Schwellwert in Register 4 ändern | thresholdregister4 | 1-4 | Number | Sets and visualizes a threshold in the given threshold register. |
|
||||
| Visualize S0 Counters | S0-Zähler anzeigen | s0input | 1-4 | Number | Visualizes the value of a S0 counter. |
|
||||
| Lock Keys Table A | Sperre Tastentabelle A | keylocktablea | 1-8 | Switch | Locks a key on the given key table and visualizes its state. |
|
||||
| Lock Keys Table B | Sperre Tastentabelle B | keylocktableb | 1-8 | Switch | Locks a key on the given key table and visualizes its state. |
|
||||
| Lock Keys Table C | Sperre Tastentabelle C | keylocktablec | 1-8 | Switch | Locks a key on the given key table and visualizes its state. |
|
||||
| Lock Keys Table D | Sperre Tastentabelle D | keylocktabled | 1-8 | Switch | Locks a key on the given key table and visualizes its state. |
|
||||
| Dimmer Output Flicker | Ausgang: Flackern | N/A | N/A | N/A | Action "flickerOutput": Let a dimmer output flicker for a given count of flashes. |
|
||||
| Dynamic Text | Dynamischer Text | N/A | N/A | N/A | Action: "sendDynamicText": Sends custom text to an LCN-GTxD display. |
|
||||
| Send Keys | Sende Tasten | N/A | N/A | N/A | Action: "hitKey": Hits a key of a key table in an LCN module. Can be used to execute commands, not supported by this binding. |
|
||||
| Dimmer Output Control Multiple | Mehrere Ausgänge steuern | output | 1-4 | Dimmer, Switch | Control multiple outputs simultaneously. See below. |
|
||||
| Transponder | Transponder | code#transponder | | Trigger | Receive transponder messages |
|
||||
| Fingerprint | Fingerprint | code#fingerprint | | Trigger | Receive fingerprint code messages |
|
||||
| Remote Control | Fernbedienung | code#remotecontrolkey | | Trigger | Receive commands from remote control |
|
||||
| Access Control | Zutrittskontrolle | code#remotecontrolcode | | Trigger | Receive serial numbers from remote control |
|
||||
| Remote Control Battery Low | Fernbedienung Batterie schwach | code#remotecontrolbatterylow | | Trigger | Triggered when the sending remote control has a low battery |
|
||||
| Host Command (Send Keys) | Kommando an Host (Sende Tasten) | hostcommand#sendKeys | - | Trigger | Receive *send keys* command from LCN module |
|
||||
| Status Message | Statusmeldungen | - | - | - | Automatically done by openHAB Binding |
|
||||
| Audio Beep | Audio Piepen | - | - | - | Not implemented |
|
||||
| Audio LCN-MRS | Audio LCN-MRS | - | - | - | Not implemented |
|
||||
| Count/Compute | Zählen/Rechnen | - | - | - | Not implemented |
|
||||
| DALI | DALI | - | - | - | Not implemented |
|
||||
| Dimmer Output Memory Toggle | Ausgang: Memory Taster | - | - | - | Not implemented |
|
||||
| Dimmer Output Ramp Stop | Ausgang: Rampe Stop | - | - | - | Not implemented |
|
||||
| Dimmer Output Relative | Ausgang: Relativ | - | - | - | Not implemented |
|
||||
| Dimmer Output Stairway | Ausgang: Treppenhauslicht | - | - | - | Not implemented |
|
||||
| Dimmer Output Timer | Ausgang: Timer (Kurzzeit) | - | - | - | Not implemented |
|
||||
| Display Set Language | Display-Sprache setzen | - | - | - | Not implemented |
|
||||
| Dynamic Groups | Dynamische Gruppen | - | - | - | Not implemented |
|
||||
| Free Input | Freie Eingabe | - | - | - | Not implemented |
|
||||
| LED Brightness | LED-Helligkeit | - | - | - | Not implemented |
|
||||
| LED Test | LED-Test | - | - | - | Not implemented |
|
||||
| LED Transform | LED-Umwandlung | - | - | - | Not implemented |
|
||||
| Light Scenes | Lichtszenen | - | - | - | Not implemented |
|
||||
| Lock Keys by Time (Table A) | Sperre (Zeit) Tasten (Tabelle A) | - | - | - | Not implemented |
|
||||
| Lock Outputs by Time | Sperre (Zeit) Ausgänge | - | - | - | Not implemented |
|
||||
| Lock Relays | Sperre Relais | - | - | - | Not implemented |
|
||||
| Lock Thresholds | Sperre Schwellwerte | - | - | - | Not implemented |
|
||||
| Motor Position | Motor Position | - | - | - | Not implemented |
|
||||
| Relay Timer | Relais-Timer | N/A | N/A | N/A | Action: "startRelayTimer": Starts a relay timer for the given relay number with the given duration in milliseconds. |
|
||||
| Send Keys Delayed | Sende Tasten verzögert | - | - | - | Not implemented |
|
||||
| Set S0 Counters | S0-Zähler setzen | - | - | - | Not implemented |
|
||||
| Status Command | Statuskommandos | - | - | - | Not implemented |
|
||||
|
||||
**For some *Channel*s a unit should be configured for visualization.** By default the native LCN value is used.
|
||||
|
||||
S0 counter Channels need to be the pulses per kWh configured. If the value is left blank, a default value of 1000 pulses/kWh is set.
|
||||
|
||||
The Rollershutter Channels provide the boolean parameter `invertUpDown`, which can be set to 'true' if the Up/Down wires are interchanged.
|
||||
|
||||
The binary sensor Channels provide the boolean parameter `invertState`, which can be set to 'true' if the binary sensor connected uses inverted logic for signaling open/closed.
|
||||
|
||||
### Transponder/Fingerprints
|
||||
|
||||
LCN transponder readers or fingerprint readers can be integrated in openHAB e.g. for access control.
|
||||
The transponder function must be enabled in the module's I-port properties within *LCN-PRO*.
|
||||
|
||||
Example: When the transponder card with the ID "12ABCD" is seen by the reader connected to LCN module "S000M011", the item "M10_Relay7" is switched on:
|
||||
|
||||
```
|
||||
rule "My Transponder"
|
||||
when
|
||||
Channel "lcn:module:b827ebfea4bb:S000M011:code#transponder" triggered "12ABCD"
|
||||
then
|
||||
M10_Relay7.sendCommand(ON)
|
||||
end
|
||||
```
|
||||
|
||||
Example: When fingerprint with ID "AFFE12" is seen by reader connected to LCN module "S000M011", the item "M10_Relay7" is switched on:
|
||||
|
||||
```
|
||||
rule "My Fingerprint"
|
||||
when
|
||||
Channel "lcn:module:b827ebfea4bb:S000M011:code#fingerprint" triggered "AFFE12"
|
||||
then
|
||||
M10_Relay7.sendCommand(ON)
|
||||
end
|
||||
```
|
||||
|
||||
### Command from an LCN Module to openHAB
|
||||
|
||||
LCN modules can send commands to openHAB, e.g. by pressing a physical LCN key.
|
||||
The command must be programmed into the LCN module by the programming software LCN-PRO.
|
||||
Only the *send keys* command (German: "Sende Tasten") is supported.
|
||||
|
||||
Program a command to a key of an LCN module via LCN-PRO.
|
||||
When LCN-PRO asks you for the target address, don't select any module, but manually enter the PCK host ID, configured within PCHK (default: 4).
|
||||
Select the *send keys* command and "A-C (former command)", as PCHK 3.2.2 only supports the old command.
|
||||
Then, select any key(s) you want to send to openHAB. These can be freely chosen, as they are only evaluated by openHAB.
|
||||
|
||||

|
||||
|
||||
The following rule can be used to trigger any action:
|
||||
|
||||
```
|
||||
rule "Module 12 sent A1 Hit"
|
||||
when
|
||||
Channel "lcn:module:b827ebfea4bb:S000M012:hostcommand#sendKeys" triggered "A1:HIT"
|
||||
then
|
||||
M10_Relay7.sendCommand(ON)
|
||||
end
|
||||
```
|
||||
|
||||
`A1` is the key of the *send keys* command, programmed by LCN-PRO.
|
||||
After the colon, the LCN "hit type" follows: HIT, MAKE or BREAK (German: kurz, lang, los)
|
||||
|
||||
If multiple keys or key tables are programmed in a single "send keys" command, multiple triggers will be executed.
|
||||
|
||||
### Remote Control
|
||||
|
||||
To evaluate commands from LCN remote controls (e.g. LCN-RT or LCN-RT16), the module's I-port behavior must be configured as "IR access control" within *LCN-PRO*:
|
||||
|
||||

|
||||
|
||||
#### Remote Control Keys
|
||||
|
||||
The trigger *Channel* `lcn:module:<pckThing>:<moduleThing>:code#remotecontrolkey` can be used to execute commands, when a specific key on a remote control is pressed:
|
||||
|
||||
```
|
||||
rule "Remote Control Key 3 on Layer 1 hit"
|
||||
when
|
||||
Channel "lcn:module:b827ebfea4bb:S000M012:code#remotecontrolkey" triggered "A3:HIT"
|
||||
then
|
||||
M10_Relay7.sendCommand(ON)
|
||||
end
|
||||
```
|
||||
|
||||
`A3` is key 3 on the first layer. `B1` is key 1 on the second layer etc.. After the colon follows the LCN "hit type" HIT, MAKE or BREAK (German: kurz, lang, los).
|
||||
|
||||
#### Remote Control used as Access Control
|
||||
|
||||
The serial number of a remote control can be used for access control via the channel `lcn:module:<pckThing>:<moduleThing>:code#remotecontrolcode`. See the following example:
|
||||
|
||||
```
|
||||
rule "Remote Control Key 3 on Layer 1 hit (only executed for serial number AB1234)"
|
||||
when
|
||||
Channel "lcn:module:b827ebfea4bb:S000M012:code#remotecontrolcode" triggered "AB1234:A3:HIT" or
|
||||
Channel "lcn:module:b827ebfea4bb:S000M012:code#remotecontrolcode" triggered "AB1234:A3:MAKE"
|
||||
then
|
||||
M10_Relay7.sendCommand(ON)
|
||||
end
|
||||
```
|
||||
|
||||
The command will be executed when the remote control button A3 is either pressed short or long.
|
||||
|
||||
## Dimmer Outputs with Ramp and Multiple Outputs
|
||||
|
||||
The *output* profile can be used to control multiple dimmer outputs of the *same* module simultaneously or control a dimmer output with a ramp (slowly dimming).
|
||||
|
||||
The optional *ramp* parameter must be float or integer.
|
||||
The lowest value is 0.25, which corresponds to 0.25s. The highest value is 486s.
|
||||
When no *ramp* parameter is specified or no profile is configured, the ramp is 0 (behavior like a switch).
|
||||
The ramp parameter is not available for Color *Item*s.
|
||||
|
||||
```
|
||||
// Dim output 2 in 0.25s
|
||||
Switch M10_Output2 {channel="lcn:module:b827ebfea4bb:S000M010:output#2"[profile="lcn:output", ramp=0.25]} // with ramp of 0.25s (smallest value)
|
||||
// Dim output 3 in 486s
|
||||
Dimmer M10_Output3 {channel="lcn:module:b827ebfea4bb:S000M010:output#3"[profile="lcn:output", ramp=486]} // with ramp of 486s (biggest value)
|
||||
```
|
||||
|
||||
The optional parameters *controlAllOutputs* and *controlOutputs12* can be used to control multiple outputs simultaneously.
|
||||
Please note that the combination of these parameters with the *ramp* parameter is limited:
|
||||
|
||||
```
|
||||
// Control outputs 1+2 simultaneously. Status of Output 1 is visualized. Only ramps of 0s or 0.25s are supported.
|
||||
Dimmer M10_Outputs12a {channel="lcn:module:b827ebfea4bb:S000M010:output#1"[profile="lcn:output", controlOutputs12=true]}
|
||||
Dimmer M10_Outputs12b {channel="lcn:module:b827ebfea4bb:S000M010:output#1"[profile="lcn:output", controlOutputs12=true, ramp=0.25]}
|
||||
// Control all outputs simultaneously. Status of Output 1 is visualized.
|
||||
Dimmer M10_OutputAll1 {channel="lcn:module:b827ebfea4bb:S000M010:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0]} // ramp only since firmware 180501
|
||||
Dimmer M10_OutputAll2 {channel="lcn:module:b827ebfea4bb:S000M010:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0.25]} // ramp compatibility: all
|
||||
Dimmer M10_OutputAll3 {channel="lcn:module:b827ebfea4bb:S000M010:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0.5]} // ramp only since firmware 180501
|
||||
```
|
||||
|
||||
## Actions
|
||||
|
||||
Actions are special commands that can be sent to LCN modules or LCN groups.
|
||||
|
||||
### Hit Key
|
||||
|
||||
This *Action* virtually hits a key of a key table in an LCN module.
|
||||
Simply spoken, openHAB acts as a push button switch connected to an LCN module.
|
||||
|
||||
This *Action* can be used to execute commands which are not natively supported by this binding.
|
||||
The function can be programmed via the software *LCN-PRO* onto a key in a module's key table.
|
||||
Then, the programmed key can be "hit" by this *Action* and the command will be executed.
|
||||
|
||||
When programming a "Hit Key" *Action*, the following parameters need to be set:
|
||||
|
||||
*table* - The module's key table: A, B, C or D<br />
|
||||
*key* - The number of the key within the key table: 1-8<br />
|
||||
*action* - The key's action: HIT (German: "kurz"), MAKE ("lang") or BREAK ("los")
|
||||
|
||||
```
|
||||
rule "Hit key C4 hourly"
|
||||
when
|
||||
Time cron "0 0 * * * ?"
|
||||
then
|
||||
val actions = getActions("lcn","lcn:module:b827ebfea4bb:S000M010")
|
||||
actions.hitKey("C", 4, "HIT")
|
||||
end
|
||||
```
|
||||
|
||||
### Dynamic Text
|
||||
|
||||
This *Action* can be used to send custom texts to an LCN-GTxD display.
|
||||
To make this function work, the row of the display has to be configured to allow dynamic text within *LCN-PRO*:
|
||||
|
||||

|
||||
|
||||
When programming a "Dynamic Text" *Action*, the following parameters need to be set:
|
||||
|
||||
*row* - The number of the row in the display: 1-4<br />
|
||||
*text* - The text to be displayed (UTF-8)
|
||||
|
||||
The length of the text may not exceed 60 bytes of characters.
|
||||
Bear in mind that unicode characters can take more than one byte (e.g. umlauts (äöü) take two bytes).
|
||||
|
||||
```
|
||||
rule "Send dynamic Text to GT10D hourly"
|
||||
when
|
||||
Time cron "0 0 * * * ?"
|
||||
then
|
||||
val actions = getActions("lcn","lcn:module:b827ebfea4bb:S000M012")
|
||||
actions.sendDynamicText(1, "Test 123 CO₂ öäü߀") // row 1
|
||||
end
|
||||
```
|
||||
|
||||
### Flicker Output
|
||||
|
||||
This *Action* realizes the LCN command "Output: Flicker" (German: "Ausgang: Flackern").
|
||||
The command let a dimmer output flash a given number of times. This feature can be used e.g. for alert signals or visual door bells.
|
||||
|
||||
When programming a "Flicker Output" *Action*, the following parameters need to be set:
|
||||
|
||||
*output* - The dimmer output number: 1-4<br />
|
||||
*depth* - The depth of the flickering: 0-2 (0=25% 1=50% 2=100% Example: When the output is fully on (100%), and 0 is selected, flashes will dim from 100% to 75% and back)<br />
|
||||
*ramp* - The duration/ramp of one flash: 0-2 (0=2sec 1=1sec 2=0.5sec)<br />
|
||||
*count* - The number of flashes: 1-15
|
||||
|
||||
This action has also effect, if the given output is off. The output will be dimmed from 0% to *depth* and back, then.
|
||||
|
||||
```
|
||||
rule "Flicker output 1 when window opens"
|
||||
when
|
||||
Item M10_BinarySensor5 changed to OPEN
|
||||
then
|
||||
val actions = getActions("lcn","lcn:module:b827ebfea4bb:S000M010")
|
||||
// output=1, depth=2=100%, ramp=0=2s, count=3
|
||||
actions.flickerOutput(1, 2, 0, 3)
|
||||
end
|
||||
```
|
||||
|
||||
### Relay Timer
|
||||
|
||||
This *Action* realizes the LCN commmand "Relay Timer" (German: "Relais-Timer").
|
||||
The command switches the given relay immediately to on and after a given time back to off.
|
||||
|
||||
When programming a "Relay Timer" *Action*, the following parameters need to be set:
|
||||
|
||||
*relayNumber* - The relay number: 1-8<br />
|
||||
*duration* - Timer duration in milliseconds: 30-240.000 ms<br />
|
||||
|
||||
```
|
||||
rule "Start relay timer for led driver when dummy switch changed"
|
||||
when
|
||||
Item Dummy_Switch changed
|
||||
then
|
||||
val actions = getActions("lcn","lcn:module:b827ebfea4bb:17B4196847")
|
||||
// relayNumber=3, duration=90
|
||||
actions.startRelayTimer(3,90)
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
## Caveat and Limitations
|
||||
|
||||
LCN segments are supported by this binding, but could not be tested, due to lack of hardware.
|
||||
|
||||
LEDs do not support the *OnOffCommand* and respectively the *Switch* Item type, because they have the additional states *BLINK* and *FLICKER*. They must be configured as *String* Item. When used in rules, the parameter must be of type string. Example: `M10_LED1.sendCommand("ON")`. Note the quotation marks.
|
||||
|
||||
## Full Example
|
||||
|
||||
Config .items
|
||||
|
||||
```
|
||||
// Dimmer Outputs
|
||||
Dimmer M10_Output1 {channel="lcn:module:b827ebfea4bb:S000M010:output#1"}
|
||||
Switch M10_Output2 {channel="lcn:module:b827ebfea4bb:S000M010:output#2"[profile="lcn:output", ramp=0.25]} // with ramp of 0.25s (smallest value)
|
||||
Dimmer M10_Output3 {channel="lcn:module:b827ebfea4bb:S000M010:output#3"[profile="lcn:output", ramp=486]} // with ramp of 486s (biggest value)
|
||||
|
||||
// Dimmer Outputs: Control all simultaneously. Status of Output 1 is visualized.
|
||||
Dimmer M10_OutputAll1 {channel="lcn:module:b827ebfea4bb:S000M010:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0]} // ramp=0: only since firmware 180501
|
||||
Dimmer M10_OutputAll2 {channel="lcn:module:b827ebfea4bb:S000M010:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0.25]} // ramp=0.25: compatibility: all firmwares
|
||||
Dimmer M10_OutputAll3 {channel="lcn:module:b827ebfea4bb:S000M010:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0.5]} // ramp>=0.5: only since firmware 180501
|
||||
|
||||
// Dimmer Outputs: Control outputs 1+2 simultaneously. Status of Output 1 is visualized. Only ramps of 0s or 0.25s are supported.
|
||||
Dimmer M10_Outputs12b {channel="lcn:module:b827ebfea4bb:S000M010:output#1"[profile="lcn:output", controlOutputs12=true, ramp=0.25]}
|
||||
|
||||
// Dimmer Outputs: RGB Control
|
||||
Color M10_Color {channel="lcn:module:b827ebfea4bb:S000M010:output#color"[profile="lcn:output"]}
|
||||
|
||||
// Roller Shutter on Output 1+2
|
||||
Rollershutter M10_RollershutterOutput1 {channel="lcn:module:b827ebfea4bb:S000M010:rollershutteroutput#1"}
|
||||
|
||||
// Relays
|
||||
Switch M10_Relay1 {channel="lcn:module:b827ebfea4bb:S000M010:relay#1"}
|
||||
|
||||
// Roller Shutter on Relays 1+2
|
||||
Rollershutter M10_RollershutterRelay1 {channel="lcn:module:b827ebfea4bb:S000M010:rollershutterrelay#1"}
|
||||
|
||||
// LEDs
|
||||
String M10_LED1 {channel="lcn:module:b827ebfea4bb:S000M010:led#1"}
|
||||
String M10_LED2 {channel="lcn:module:b827ebfea4bb:S000M010:led#2"}
|
||||
|
||||
// Logic Operations (legacy name: "Sums")
|
||||
String M10_Logic1 {channel="lcn:module:b827ebfea4bb:S000M010:logic#1"}
|
||||
String M10_Logic2 {channel="lcn:module:b827ebfea4bb:S000M010:logic#2"[profile="transform:MAP", function="alertSystem.map"]}
|
||||
// conf/transform/alertSystem.map:
|
||||
// NOT=All windows are closed
|
||||
// OR=Some windows are open
|
||||
// AND=All windows are open
|
||||
|
||||
// Binary Sensors
|
||||
Contact M10_BinarySensor1 {channel="lcn:module:b827ebfea4bb:S000M010:binarysensor#1"}
|
||||
|
||||
// Variables
|
||||
// The units of the variables must also be set in the Channels configuration, to be visualized correctly.
|
||||
Number:Temperature M10_Variable1 "[%.1f %unit%]" <temperature> {channel="lcn:module:b827ebfea4bb:S000M010:variable#1"} // Temperature in °C
|
||||
Number:Temperature M10_Variable2 "[%.1f °F]" <temperature> {channel="lcn:module:b827ebfea4bb:S000M010:variable#2"} // Temperature in °F
|
||||
Number M10_Variable3 "[%d ppm]" <temperature> {channel="lcn:module:b827ebfea4bb:S000M010:variable#3"} // Indoor air quality in ppm
|
||||
Number M10_Variable4 "[%d lx]" {channel="lcn:module:b827ebfea4bb:S000M010:variable#4"} // Illuminance in Lux
|
||||
Number:Illuminance M10_Variable5 "[%.1f klx]" {channel="lcn:module:b827ebfea4bb:S000M010:variable#5"} // Illuminance in kLux
|
||||
Number M10_Variable6 "[%.1f mA]" {channel="lcn:module:b827ebfea4bb:S000M010:variable#6"} // Electrical current in mA
|
||||
Number M10_Variable7 "[%.1f V]" {channel="lcn:module:b827ebfea4bb:S000M010:variable#7"} // Voltage in V
|
||||
Number M10_Variable8 "[%.1f m/s]" {channel="lcn:module:b827ebfea4bb:S000M010:variable#8"} // Wind speed in m/s
|
||||
Number M10_Variable9 "[%.1f °]" {channel="lcn:module:b827ebfea4bb:S000M010:variable#9"} // position of the sun (azimuth or elevation) in °
|
||||
Number M10_Variable10 "[%d W]" {channel="lcn:module:b827ebfea4bb:S000M010:variable#10"} // Current power of an S0 input in W
|
||||
Number:Power M10_Variable11 "[%.1f kW]" {channel="lcn:module:b827ebfea4bb:S000M010:variable#11"} // Current power of an S0 input in kW
|
||||
|
||||
// Regulators
|
||||
Number:Temperature M10_R1VarSetpoint "[%.1f %unit%]" <temperature> {channel="lcn:module:b827ebfea4bb:S000M010:rvarsetpoint#1"} // Temperature in °C
|
||||
Switch M10_R1VarLock {channel="lcn:module:b827ebfea4bb:S000M010:rvarlock#1"} // Lock state of R1Var
|
||||
|
||||
// Thresholds
|
||||
Number:Temperature M10_ThresholdRegister1_Threshold1 "[%.1f %unit%]" {channel="lcn:module:b827ebfea4bb:S000M010:thresholdregister1#1"} // Temperature in °C
|
||||
Number:Temperature M10_ThresholdRegister4_Threshold2 "[%.1f %unit%]" {channel="lcn:module:b827ebfea4bb:S000M010:thresholdregister4#2"} // Temperature in °C
|
||||
|
||||
// S0 Counters
|
||||
Number:Energy M10_S0Counter1 "[%.1f kWh]" {channel="lcn:module:b827ebfea4bb:S000M010:s0input#1"}
|
||||
|
||||
// Key Locks
|
||||
Switch M10_KeyLockA1 {channel="lcn:module:b827ebfea4bb:S000M010:keylocktablea#1"}
|
||||
Switch M10_KeyLockD5 {channel="lcn:module:b827ebfea4bb:S000M010:keylocktabled#5"}
|
||||
```
|
||||
|
||||
Config .sitemap
|
||||
|
||||
```
|
||||
sitemap lcn label="My home automation" {
|
||||
Frame label="Demo Items" {
|
||||
// Dimmer Outputs
|
||||
Default item=M10_Output1 label="Output 1"
|
||||
Default item=M10_Output2 label="Output 2"
|
||||
Default item=M10_Output3 label="Output 3"
|
||||
|
||||
// Dimmer Outputs: Control all simultaneously. Status of Output 1 is visualized.
|
||||
Default item=M10_OutputAll1 label="All Outputs ramp=0 since firmware 180501"
|
||||
Default item=M10_OutputAll2 label="All Outputs ramp=250ms all firmwares"
|
||||
Default item=M10_OutputAll3 label="All Outputs ramp>=500ms since firmware 180501"
|
||||
|
||||
// Dimmer Outputs: Control outputs 1+2 simultaneously. Status of Output 1 is visualized. Only ramps of 0s or 0.25s are supported.
|
||||
Default item=M10_Outputs12a label="Outputs 1+2 Ramp=0"
|
||||
Default item=M10_Outputs12b label="Outputs 1+2 Ramp=0.25s"
|
||||
|
||||
// Dimmer Outputs: RGB Control
|
||||
Colorpicker item=M10_Color
|
||||
|
||||
// Roller Shutter on Outputs 1+2
|
||||
Default item=M10_RollershutterOutput1 label="Roller Shutter on Output 1+2"
|
||||
|
||||
// Relays
|
||||
Default item=M10_Relay1 label="Relay 1"
|
||||
|
||||
// Roller Shutter on Relays
|
||||
Default item=M10_RollershutterRelay1 label="Roller Shutter on Relay 1-2"
|
||||
|
||||
// LEDs
|
||||
Switch item=M10_LED1 label="LED 1" mappings=[ON=ON, OFF=OFF] // Don't display "Blink" or "Flicker"
|
||||
Switch item=M10_LED2 label="LED 2"
|
||||
|
||||
// Logic Operations (legacy name: "Sums")
|
||||
Default item=M10_Logic1 label="Logic Operation 1"
|
||||
Default item=M10_Logic2 label="Logic Operation 2"
|
||||
|
||||
// Binary Sensors
|
||||
Default item=M10_BinarySensor1 label="Binary Sensor 1"
|
||||
|
||||
// Variables
|
||||
Setpoint item=M10_Variable1 label="Variable 1"
|
||||
Default item=M10_Variable2 label="Variable 2"
|
||||
Default item=M10_Variable3 label="Variable 3"
|
||||
Default item=M10_Variable4 label="Variable 4"
|
||||
Default item=M10_Variable5 label="Variable 5"
|
||||
Default item=M10_Variable6 label="Variable 6"
|
||||
Default item=M10_Variable7 label="Variable 7"
|
||||
Default item=M10_Variable8 label="Variable 8"
|
||||
Default item=M10_Variable9 label="Variable 9"
|
||||
Default item=M10_Variable10 label="Variable 10"
|
||||
Default item=M10_Variable11 label="Variable 11"
|
||||
|
||||
// Regulators
|
||||
Setpoint item=M10_R1VarSetpoint label="R1Var Setpoint" step=1 minValue=-10.0
|
||||
Default item=M10_R1VarLock label="R1Var Lock" // Lock state of R1Var
|
||||
|
||||
// Thresholds
|
||||
Setpoint item=M10_ThresholdRegister1_Threshold1 label="Threshold Register 1 Threshold 1"
|
||||
Setpoint item=M10_ThresholdRegister4_Threshold2 label="Threshold Register 4 Threshold 2"
|
||||
|
||||
// S0 Counters
|
||||
Default item=M10_S0Counter1 label="S0 Counter 1"
|
||||
|
||||
// Key Locks
|
||||
Default item=M10_KeyLockA1 label="Locked State Key A1"
|
||||
Default item=M10_KeyLockD5 label="Locked State Key D5"
|
||||
}
|
||||
}
|
||||
```
|
||||
BIN
bundles/org.openhab.binding.lcn/doc/LCN-PRO_output_steps.png
Normal file
BIN
bundles/org.openhab.binding.lcn/doc/LCN-PRO_output_steps.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
BIN
bundles/org.openhab.binding.lcn/doc/dyn_text.png
Normal file
BIN
bundles/org.openhab.binding.lcn/doc/dyn_text.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
BIN
bundles/org.openhab.binding.lcn/doc/host_command_send_keys.png
Normal file
BIN
bundles/org.openhab.binding.lcn/doc/host_command_send_keys.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
BIN
bundles/org.openhab.binding.lcn/doc/ir.png
Normal file
BIN
bundles/org.openhab.binding.lcn/doc/ir.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
BIN
bundles/org.openhab.binding.lcn/doc/overview.jpg
Normal file
BIN
bundles/org.openhab.binding.lcn/doc/overview.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
17
bundles/org.openhab.binding.lcn/pom.xml
Normal file
17
bundles/org.openhab.binding.lcn/pom.xml
Normal 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.lcn</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: LCN Binding</name>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.lcn-${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-lcn" description="Lcn Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.lcn/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.DimmerOutputCommand;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.profiles.ProfileCallback;
|
||||
import org.openhab.core.thing.profiles.ProfileContext;
|
||||
import org.openhab.core.thing.profiles.ProfileTypeUID;
|
||||
import org.openhab.core.thing.profiles.StateProfile;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A profile to control multiple dimmer outputs simultaneously with ramp.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DimmerOutputProfile implements StateProfile {
|
||||
private final Logger logger = LoggerFactory.getLogger(DimmerOutputProfile.class);
|
||||
/** The Profile's UID */
|
||||
static final ProfileTypeUID UID = new ProfileTypeUID(LcnBindingConstants.BINDING_ID, "output");
|
||||
private final ProfileCallback callback;
|
||||
private int rampMs;
|
||||
private boolean controlAllOutputs;
|
||||
private boolean controlOutputs12;
|
||||
|
||||
public DimmerOutputProfile(ProfileCallback callback, ProfileContext profileContext) {
|
||||
this.callback = callback;
|
||||
|
||||
Optional<Object> ramp = getConfig(profileContext, "ramp");
|
||||
Optional<Object> allOutputs = getConfig(profileContext, "controlAllOutputs");
|
||||
Optional<Object> outputs12 = getConfig(profileContext, "controlOutputs12");
|
||||
|
||||
ramp.ifPresent(b -> {
|
||||
if (b instanceof BigDecimal) {
|
||||
rampMs = (int) (((BigDecimal) b).doubleValue() * 1000);
|
||||
} else {
|
||||
logger.warn("Could not parse 'ramp', unexpected type, should be float: {}", ramp);
|
||||
}
|
||||
});
|
||||
|
||||
allOutputs.ifPresent(b -> {
|
||||
if (b instanceof Boolean) {
|
||||
controlAllOutputs = true;
|
||||
} else {
|
||||
logger.warn("Could not parse 'controlAllOutputs', unexpected type, should be true/false: {}", b);
|
||||
}
|
||||
});
|
||||
|
||||
outputs12.ifPresent(b -> {
|
||||
if (b instanceof Boolean) {
|
||||
controlOutputs12 = true;
|
||||
} else {
|
||||
logger.warn("Could not parse 'controlOutputs12', unexpected type, should be true/false: {}", b);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Optional<Object> getConfig(ProfileContext profileContext, String key) {
|
||||
return Optional.ofNullable(profileContext.getConfiguration().get(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCommandFromItem(Command command) {
|
||||
if (rampMs != 0 && rampMs != LcnDefs.FIXED_RAMP_MS && controlOutputs12) {
|
||||
logger.warn("Unsupported 'ramp' setting. Will be forced to 250ms: {}", rampMs);
|
||||
}
|
||||
BigDecimal value;
|
||||
if (command instanceof DecimalType) {
|
||||
value = ((DecimalType) command).toBigDecimal();
|
||||
} else if (command instanceof OnOffType) {
|
||||
value = ((OnOffType) command) == OnOffType.ON ? BigDecimal.valueOf(100) : BigDecimal.ZERO;
|
||||
} else {
|
||||
logger.warn("Unsupported type: {}", command.toFullString());
|
||||
return;
|
||||
}
|
||||
callback.handleCommand(new DimmerOutputCommand(value, controlAllOutputs, controlOutputs12, rampMs));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateUpdateFromHandler(State state) {
|
||||
callback.sendUpdate(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProfileTypeUID getProfileTypeUID() {
|
||||
return UID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCommandFromHandler(Command command) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateUpdateFromItem(State state) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link ILcnModuleActions} defines the interface for all thing actions supported by the binding.
|
||||
* These methods, parameters, and return types are explained in {@link LcnModuleActions}.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ILcnModuleActions {
|
||||
void hitKey(@Nullable String table, int key, @Nullable String action);
|
||||
|
||||
void flickerOutput(int output, int depth, int ramp, int count);
|
||||
|
||||
void sendDynamicText(int row, @Nullable String textInput);
|
||||
|
||||
void startRelayTimer(int relaynumber, double duration);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link LcnBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnBindingConstants {
|
||||
/** The scope name of this binding */
|
||||
public static final String BINDING_ID = "lcn";
|
||||
/**
|
||||
* Firmware version of the measurement processing since 2013. It has more variables and thresholds and event-based
|
||||
* variable updates.
|
||||
*/
|
||||
public static final int FIRMWARE_2013 = 0x170206;
|
||||
/** Firmware version which supports controlling all 4 outputs simultaneously */
|
||||
public static final int FIRMWARE_2014 = 0x180501;
|
||||
/** List of all Thing Type UIDs */
|
||||
public static final ThingTypeUID THING_TYPE_PCK_GATEWAY = new ThingTypeUID(BINDING_ID, "pckGateway");
|
||||
public static final ThingTypeUID THING_TYPE_MODULE = new ThingTypeUID(BINDING_ID, "module");
|
||||
public static final ThingTypeUID THING_TYPE_GROUP = new ThingTypeUID(BINDING_ID, "group");
|
||||
/** Regex for address in PCK protocol */
|
||||
public static final String ADDRESS_REGEX = "[:=%]M(?<segId>\\d{3})(?<modId>\\d{3})";
|
||||
/** LCN coding for ACK */
|
||||
public static final int CODE_ACK = -1;
|
||||
}
|
||||
@@ -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.lcn.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link LcnChannelVariableConfiguration} class contains configuration field mapping for Channels of type
|
||||
* 'variable'.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnChannelVariableConfiguration {
|
||||
public String unit = "native";
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link LcnModuleConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnGroupConfiguration extends LcnModuleConfiguration {
|
||||
public int groupId;
|
||||
}
|
||||
@@ -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.lcn.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddr;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddrGrp;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.core.thing.Thing;
|
||||
|
||||
/**
|
||||
* The {@link LcnGroupHandler} is responsible for handling commands, which are
|
||||
* addressed to an LCN group.
|
||||
*
|
||||
* The module in the field moduleAddress is used for state updates of the group as representative for all modules in
|
||||
* the group.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnGroupHandler extends LcnModuleHandler {
|
||||
private @Nullable LcnAddrGrp groupAddress;
|
||||
|
||||
public LcnGroupHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
LcnGroupConfiguration localConfig = getConfigAs(LcnGroupConfiguration.class);
|
||||
groupAddress = new LcnAddrGrp(localConfig.segmentId, localConfig.groupId);
|
||||
|
||||
super.initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void requestFirmwareVersionAndSerialNumberIfNotSet() throws LcnException {
|
||||
// nothing, don't request the serial number of an LCN group representation module
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LcnAddr getCommandAddress() throws LcnException {
|
||||
LcnAddrGrp localAddress = groupAddress;
|
||||
if (localAddress == null) {
|
||||
throw new LcnException("LCN group address not set");
|
||||
}
|
||||
return localAddress;
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal;
|
||||
|
||||
import static org.openhab.binding.lcn.internal.LcnBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
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.Component;
|
||||
|
||||
/**
|
||||
* The {@link LcnHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.lcn", service = ThingHandlerFactory.class)
|
||||
public class LcnHandlerFactory extends BaseThingHandlerFactory {
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
|
||||
Stream.of(THING_TYPE_PCK_GATEWAY, THING_TYPE_MODULE, THING_TYPE_GROUP).collect(Collectors.toSet()));
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_GROUP.equals(thingTypeUID)) {
|
||||
return new LcnGroupHandler(thing);
|
||||
}
|
||||
|
||||
if (THING_TYPE_MODULE.equals(thingTypeUID)) {
|
||||
return new LcnModuleHandler(thing);
|
||||
}
|
||||
|
||||
if (THING_TYPE_PCK_GATEWAY.equals(thingTypeUID)) {
|
||||
return new PckGatewayHandler((Bridge) thing);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs.KeyTable;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs.SendKeyCommand;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
import org.openhab.core.automation.annotation.ActionInput;
|
||||
import org.openhab.core.automation.annotation.RuleAction;
|
||||
import org.openhab.core.thing.binding.ThingActions;
|
||||
import org.openhab.core.thing.binding.ThingActionsScope;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Handles actions requested to be sent to an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@ThingActionsScope(name = "lcn")
|
||||
@NonNullByDefault
|
||||
public class LcnModuleActions implements ThingActions, ILcnModuleActions {
|
||||
private final Logger logger = LoggerFactory.getLogger(LcnModuleActions.class);
|
||||
private static final int DYN_TEXT_CHUNK_COUNT = 5;
|
||||
private static final int DYN_TEXT_HEADER_LENGTH = 6;
|
||||
private static final int DYN_TEXT_CHUNK_LENGTH = 12;
|
||||
private @Nullable LcnModuleHandler moduleHandler;
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
this.moduleHandler = (LcnModuleHandler) handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return moduleHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
@RuleAction(label = "LCN Hit Key", description = "Sends a \"hit key\" command to an LCN module")
|
||||
public void hitKey(
|
||||
@ActionInput(name = "table", required = true, type = "java.lang.String", label = "Table", description = "The key table (A-D)") @Nullable String table,
|
||||
@ActionInput(name = "key", required = true, type = "java.lang.Integer", label = "Key", description = "The key number (1-8)") int key,
|
||||
@ActionInput(name = "action", required = true, type = "java.lang.String", label = "Action", description = "The action (HIT, MAKE, BREAK)") @Nullable String action) {
|
||||
try {
|
||||
if (table == null) {
|
||||
throw new LcnException("Table is not set");
|
||||
}
|
||||
|
||||
if (action == null) {
|
||||
throw new LcnException("Action is not set");
|
||||
}
|
||||
|
||||
KeyTable keyTable;
|
||||
try {
|
||||
keyTable = LcnDefs.KeyTable.valueOf(table.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new LcnException("Unknown key table: " + table);
|
||||
}
|
||||
|
||||
SendKeyCommand sendKeyCommand;
|
||||
try {
|
||||
sendKeyCommand = SendKeyCommand.valueOf(action.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new LcnException("Unknown action: " + action);
|
||||
}
|
||||
|
||||
if (!LcnChannelGroup.KEYLOCKTABLEA.isValidId(key - 1)) {
|
||||
throw new LcnException("Key number is out of range: " + key);
|
||||
}
|
||||
|
||||
SendKeyCommand[] cmds = new SendKeyCommand[LcnDefs.KEY_TABLE_COUNT];
|
||||
Arrays.fill(cmds, SendKeyCommand.DONTSEND);
|
||||
boolean[] keys = new boolean[LcnChannelGroup.KEYLOCKTABLEA.getCount()];
|
||||
|
||||
int keyTableNumber = keyTable.name().charAt(0) - LcnDefs.KeyTable.A.name().charAt(0);
|
||||
cmds[keyTableNumber] = sendKeyCommand;
|
||||
keys[key - 1] = true;
|
||||
|
||||
getHandler().sendPck(PckGenerator.sendKeys(cmds, keys));
|
||||
} catch (LcnException e) {
|
||||
logger.warn("Could not execute hit key command: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RuleAction(label = "LCN Flicker Output", description = "Let a dimmer output flicker for a given count of flashes")
|
||||
public void flickerOutput(
|
||||
@ActionInput(name = "output", type = "java.lang.Integer", required = true, label = "Output", description = "The output number (1-4)") int output,
|
||||
@ActionInput(name = "depth", type = "java.lang.Integer", label = "Depth", description = "0=25% 1=50% 2=100%") int depth,
|
||||
@ActionInput(name = "ramp", type = "java.lang.Integer", label = "Ramp", description = "0=2sec 1=1sec 2=0.5sec") int ramp,
|
||||
@ActionInput(name = "count", type = "java.lang.Integer", label = "Count", description = "Number of flashes (1-15)") int count) {
|
||||
try {
|
||||
getHandler().sendPck(PckGenerator.flickerOutput(output - 1, depth, ramp, count));
|
||||
} catch (LcnException e) {
|
||||
logger.warn("Could not send output flicker command: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RuleAction(label = "LCN Dynamic Text", description = "Send custom text to an LCN-GTxD display")
|
||||
public void sendDynamicText(
|
||||
@ActionInput(name = "row", type = "java.lang.Integer", required = true, label = "Row", description = "Display the text on the LCN-GTxD in the given row number (1-4)") int row,
|
||||
@ActionInput(name = "text", type = "java.lang.String", label = "Text", description = "The text to display (max. 60 chars/bytes)") @Nullable String textInput) {
|
||||
try {
|
||||
String text = textInput;
|
||||
|
||||
if (text == null) {
|
||||
text = new String();
|
||||
}
|
||||
|
||||
// convert String to bytes to split the data every 12 bytes, because a unicode character can take more than
|
||||
// one byte
|
||||
ByteBuffer bb = ByteBuffer.wrap(text.getBytes(LcnDefs.LCN_ENCODING));
|
||||
|
||||
if (bb.capacity() > DYN_TEXT_CHUNK_LENGTH * DYN_TEXT_CHUNK_COUNT) {
|
||||
logger.warn("Dynamic text truncated. Has {} bytes: '{}'", bb.capacity(), text);
|
||||
}
|
||||
|
||||
bb.limit(Math.min(DYN_TEXT_CHUNK_LENGTH * DYN_TEXT_CHUNK_COUNT, bb.capacity()));
|
||||
|
||||
int part = 0;
|
||||
while (bb.hasRemaining()) {
|
||||
byte[] chunk = new byte[DYN_TEXT_CHUNK_LENGTH];
|
||||
bb.get(chunk, 0, Math.min(bb.remaining(), DYN_TEXT_CHUNK_LENGTH));
|
||||
|
||||
ByteBuffer command = ByteBuffer.allocate(DYN_TEXT_HEADER_LENGTH + DYN_TEXT_CHUNK_LENGTH);
|
||||
command.put(PckGenerator.dynTextHeader(row - 1, part++).getBytes(LcnDefs.LCN_ENCODING));
|
||||
command.put(chunk);
|
||||
|
||||
getHandler().sendPck(command.array());
|
||||
}
|
||||
} catch (IllegalArgumentException | LcnException e) {
|
||||
logger.warn("Could not send dynamic text: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an lcn relay timer with the given duration [ms]
|
||||
*
|
||||
* @param relaynumber 1-based number of the relay to use
|
||||
* @param duration duration of the relay timer in milliseconds
|
||||
*/
|
||||
@Override
|
||||
@RuleAction(label = "LCN Relay Timer", description = "Start an LCN relay timer")
|
||||
public void startRelayTimer(
|
||||
@ActionInput(name = "relaynumber", required = true, type = "java.lang.Integer", label = "Relay Number", description = "The relay number (1-8)") int relayNumber,
|
||||
@ActionInput(name = "duration", required = true, type = "java.lang.Double", label = "Duration [ms]", description = "The timer duration in milliseconds") double duration) {
|
||||
try {
|
||||
getHandler().sendPck(PckGenerator.startRelayTimer(relayNumber, duration));
|
||||
} catch (LcnException e) {
|
||||
logger.warn("Could not send start relay timer command: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static ILcnModuleActions invokeMethodOf(@Nullable ThingActions actions) {
|
||||
if (actions == null) {
|
||||
throw new IllegalArgumentException("actions cannot be null");
|
||||
}
|
||||
if (actions.getClass().getName().equals(LcnModuleActions.class.getName())) {
|
||||
if (actions instanceof LcnModuleActions) {
|
||||
return (ILcnModuleActions) actions;
|
||||
} else {
|
||||
return (ILcnModuleActions) Proxy.newProxyInstance(ILcnModuleActions.class.getClassLoader(),
|
||||
new Class[] { ILcnModuleActions.class }, (Object proxy, Method method, Object[] args) -> {
|
||||
Method m = actions.getClass().getDeclaredMethod(method.getName(),
|
||||
method.getParameterTypes());
|
||||
return m.invoke(actions, args);
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Actions is not an instance of LcnModuleActions");
|
||||
}
|
||||
|
||||
/** Static alias to support the old DSL rules engine and make the action available there. */
|
||||
public static void hitKey(@Nullable ThingActions actions, @Nullable String table, int key,
|
||||
@Nullable String action) {
|
||||
invokeMethodOf(actions).hitKey(table, key, action);
|
||||
}
|
||||
|
||||
/** Static alias to support the old DSL rules engine and make the action available there. */
|
||||
public static void flickerOutput(@Nullable ThingActions actions, int output, int depth, int ramp, int count) {
|
||||
invokeMethodOf(actions).flickerOutput(output, depth, ramp, count);
|
||||
}
|
||||
|
||||
/** Static alias to support the old DSL rules engine and make the action available there. */
|
||||
public static void sendDynamicText(@Nullable ThingActions actions, int row, @Nullable String text) {
|
||||
invokeMethodOf(actions).sendDynamicText(row, text);
|
||||
}
|
||||
|
||||
/** Static alias to support the old DSL rules engine and make the action available there. */
|
||||
public static void startRelayTimer(@Nullable ThingActions actions, int relaynumber, double duration) {
|
||||
invokeMethodOf(actions).startRelayTimer(relaynumber, duration);
|
||||
}
|
||||
|
||||
private LcnModuleHandler getHandler() throws LcnException {
|
||||
LcnModuleHandler localModuleHandler = moduleHandler;
|
||||
if (localModuleHandler != null) {
|
||||
return localModuleHandler;
|
||||
} else {
|
||||
throw new LcnException("Handler not set");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link LcnModuleConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleConfiguration {
|
||||
public int segmentId;
|
||||
public int moduleId;
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddrMod;
|
||||
import org.openhab.binding.lcn.internal.connection.Connection;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleMetaAckSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleMetaFirmwareSubHandler;
|
||||
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.thing.Thing;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Scans all LCN segments for LCN modules.
|
||||
*
|
||||
* Scan approach:
|
||||
* 1. Send "Leerkomando" to the broadcast address with request for Ack set
|
||||
* 2. For every received Ack, send the following requests to the module:
|
||||
* - serial number request (SN)
|
||||
* - module's name first part request (NM1)
|
||||
* - module's name second part request (NM2)
|
||||
* 3. When all three messages have been received, fire thingDiscovered()
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleDiscoveryService extends AbstractDiscoveryService
|
||||
implements DiscoveryService, ThingHandlerService {
|
||||
private final Logger logger = LoggerFactory.getLogger(LcnModuleDiscoveryService.class);
|
||||
private static final Pattern NAME_PATTERN = Pattern
|
||||
.compile("=M(?<segId>\\d{3})(?<modId>\\d{3}).N(?<part>[1-2]{1})(?<name>.*)");
|
||||
private static final String SEGMENT_ID = "segmentId";
|
||||
private static final String MODULE_ID = "moduleId";
|
||||
private static final int MODULE_NAME_PART_COUNT = 2;
|
||||
private static final int DISCOVERY_TIMEOUT_SEC = 90;
|
||||
private static final int ACK_TIMEOUT_MS = 1000;
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
|
||||
.unmodifiableSet(Stream.of(LcnBindingConstants.THING_TYPE_MODULE).collect(Collectors.toSet()));
|
||||
private @Nullable PckGatewayHandler bridgeHandler;
|
||||
private final Map<LcnAddrMod, @Nullable Map<Integer, String>> moduleNames = new HashMap<>();
|
||||
private final Map<LcnAddrMod, DiscoveryResultBuilder> discoveryResultBuilders = new ConcurrentHashMap<>();
|
||||
private final List<LcnAddrMod> successfullyDiscovered = new LinkedList<>();
|
||||
private final Queue<@Nullable LcnAddrMod> serialNumberRequestQueue = new ConcurrentLinkedQueue<>();
|
||||
private final Queue<@Nullable LcnAddrMod> moduleNameRequestQueue = new ConcurrentLinkedQueue<>();
|
||||
private @Nullable volatile ScheduledFuture<?> queueProcessor;
|
||||
private @Nullable ScheduledFuture<?> builderTask;
|
||||
|
||||
public LcnModuleDiscoveryService() {
|
||||
super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SEC, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
if (handler instanceof PckGatewayHandler) {
|
||||
this.bridgeHandler = (PckGatewayHandler) handler;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return bridgeHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
stopScan();
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
synchronized (this) {
|
||||
PckGatewayHandler localBridgeHandler = bridgeHandler;
|
||||
if (localBridgeHandler == null) {
|
||||
logger.warn("Bridge handler not set");
|
||||
return;
|
||||
}
|
||||
|
||||
ScheduledFuture<?> localBuilderTask = builderTask;
|
||||
if (localBridgeHandler.getConnection() == null && localBuilderTask != null) {
|
||||
localBuilderTask.cancel(true);
|
||||
}
|
||||
|
||||
localBridgeHandler.registerPckListener(data -> {
|
||||
Matcher matcher;
|
||||
|
||||
if ((matcher = LcnModuleMetaAckSubHandler.PATTERN_POS.matcher(data)).matches()
|
||||
|| (matcher = LcnModuleMetaFirmwareSubHandler.PATTERN.matcher(data)).matches()
|
||||
|| (matcher = NAME_PATTERN.matcher(data)).matches()) {
|
||||
synchronized (LcnModuleDiscoveryService.this) {
|
||||
Connection connection = localBridgeHandler.getConnection();
|
||||
|
||||
if (connection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
LcnAddrMod addr = new LcnAddrMod(
|
||||
localBridgeHandler.toLogicalSegmentId(Integer.parseInt(matcher.group("segId"))),
|
||||
Integer.parseInt(matcher.group("modId")));
|
||||
|
||||
if (matcher.pattern() == LcnModuleMetaAckSubHandler.PATTERN_POS) {
|
||||
// Received an ACK frame
|
||||
|
||||
// The module could send an Ack with a response to another command. So, ignore the Ack, when
|
||||
// we received our data already.
|
||||
if (!discoveryResultBuilders.containsKey(addr)) {
|
||||
serialNumberRequestQueue.add(addr);
|
||||
rescheduleQueueProcessor(); // delay request of serial until all modules finished ACKing
|
||||
}
|
||||
|
||||
Map<Integer, String> localNameParts = moduleNames.get(addr);
|
||||
if (localNameParts == null || localNameParts.size() != MODULE_NAME_PART_COUNT) {
|
||||
moduleNameRequestQueue.add(addr);
|
||||
rescheduleQueueProcessor(); // delay request of names until all modules finished ACKing
|
||||
}
|
||||
} else if (matcher.pattern() == LcnModuleMetaFirmwareSubHandler.PATTERN) {
|
||||
// Received a firmware version info frame
|
||||
|
||||
ThingUID bridgeUid = localBridgeHandler.getThing().getUID();
|
||||
String serialNumber = matcher.group("sn");
|
||||
|
||||
String thingID = String.format("S%03dM%03d", addr.getSegmentId(), addr.getModuleId());
|
||||
|
||||
ThingUID thingUid = new ThingUID(LcnBindingConstants.THING_TYPE_MODULE, bridgeUid, thingID);
|
||||
|
||||
Map<String, Object> properties = new HashMap<>(3);
|
||||
properties.put(SEGMENT_ID, addr.getSegmentId());
|
||||
properties.put(MODULE_ID, addr.getModuleId());
|
||||
properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
|
||||
|
||||
DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid)
|
||||
.withProperties(properties).withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER)
|
||||
.withBridge(bridgeUid);
|
||||
|
||||
discoveryResultBuilders.put(addr, discoveryResult);
|
||||
} else if (matcher.pattern() == NAME_PATTERN) {
|
||||
// Received part of a module's name frame
|
||||
|
||||
final int part = Integer.parseInt(matcher.group("part")) - 1;
|
||||
final String name = matcher.group("name");
|
||||
|
||||
moduleNames.compute(addr, (partNumber, namePart) -> {
|
||||
Map<Integer, String> namePartMapping = namePart;
|
||||
if (namePartMapping == null) {
|
||||
namePartMapping = new HashMap<>();
|
||||
}
|
||||
|
||||
namePartMapping.put(part, name);
|
||||
|
||||
return namePartMapping;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
builderTask = scheduler.scheduleWithFixedDelay(() -> {
|
||||
synchronized (LcnModuleDiscoveryService.this) {
|
||||
discoveryResultBuilders.entrySet().stream().filter(e -> {
|
||||
Map<Integer, String> localNameParts = moduleNames.get(e.getKey());
|
||||
return localNameParts != null && localNameParts.size() == MODULE_NAME_PART_COUNT;
|
||||
}).filter(e -> !successfullyDiscovered.contains(e.getKey())).forEach(e -> {
|
||||
StringBuilder thingName = new StringBuilder();
|
||||
if (e.getKey().getSegmentId() != 0) {
|
||||
thingName.append("Segment " + e.getKey().getSegmentId() + " ");
|
||||
}
|
||||
|
||||
thingName.append("Module " + e.getKey().getModuleId() + ": ");
|
||||
Map<Integer, String> localNameParts = moduleNames.get(e.getKey());
|
||||
if (localNameParts != null) {
|
||||
thingName.append(localNameParts.get(0));
|
||||
thingName.append(localNameParts.get(1));
|
||||
|
||||
thingDiscovered(e.getValue().withLabel(thingName.toString()).build());
|
||||
successfullyDiscovered.add(e.getKey());
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 500, 500, TimeUnit.MILLISECONDS);
|
||||
|
||||
localBridgeHandler.sendModuleDiscoveryCommand();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void rescheduleQueueProcessor() {
|
||||
// delay serial number and module name requests to not clog the bus
|
||||
ScheduledFuture<?> localQueueProcessor = queueProcessor;
|
||||
if (localQueueProcessor != null) {
|
||||
localQueueProcessor.cancel(true);
|
||||
}
|
||||
queueProcessor = scheduler.scheduleWithFixedDelay(() -> {
|
||||
PckGatewayHandler localBridgeHandler = bridgeHandler;
|
||||
if (localBridgeHandler != null) {
|
||||
LcnAddrMod serial = serialNumberRequestQueue.poll();
|
||||
if (serial != null) {
|
||||
localBridgeHandler.sendSerialNumberRequest(serial);
|
||||
}
|
||||
|
||||
LcnAddrMod name = moduleNameRequestQueue.poll();
|
||||
if (name != null) {
|
||||
localBridgeHandler.sendModuleNameRequest(name);
|
||||
}
|
||||
|
||||
// stop scan when all LCN modules have been requested
|
||||
if (serial == null && name == null) {
|
||||
scheduler.schedule(this::stopScan, ACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
}, ACK_TIMEOUT_MS, ACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stopScan() {
|
||||
ScheduledFuture<?> localBuilderTask = builderTask;
|
||||
if (localBuilderTask != null) {
|
||||
localBuilderTask.cancel(true);
|
||||
}
|
||||
ScheduledFuture<?> localQueueProcessor = queueProcessor;
|
||||
if (localQueueProcessor != null) {
|
||||
localQueueProcessor.cancel(true);
|
||||
}
|
||||
PckGatewayHandler localBridgeHandler = bridgeHandler;
|
||||
if (localBridgeHandler != null) {
|
||||
localBridgeHandler.removeAllPckListeners();
|
||||
}
|
||||
successfullyDiscovered.clear();
|
||||
moduleNames.clear();
|
||||
|
||||
super.stopScan();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.lcn.internal.common.DimmerOutputCommand;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddr;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddrMod;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.connection.Connection;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.binding.lcn.internal.converter.Converter;
|
||||
import org.openhab.binding.lcn.internal.converter.Converters;
|
||||
import org.openhab.binding.lcn.internal.converter.InversionConverter;
|
||||
import org.openhab.binding.lcn.internal.converter.S0Converter;
|
||||
import org.openhab.binding.lcn.internal.subhandler.AbstractLcnModuleSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleMetaAckSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleMetaFirmwareSubHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StopMoveType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
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.ThingHandlerService;
|
||||
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 LcnModuleHandler} is responsible for handling commands, which are
|
||||
* sent to or received from one of the channels.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleHandler extends BaseThingHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(LcnModuleHandler.class);
|
||||
private static final Map<String, Converter> VALUE_CONVERTERS = new HashMap<>();
|
||||
private static final InversionConverter INVERSION_CONVERTER = new InversionConverter();
|
||||
private @Nullable LcnAddrMod moduleAddress;
|
||||
private final Map<LcnChannelGroup, @Nullable AbstractLcnModuleSubHandler> subHandlers = new HashMap<>();
|
||||
private final List<AbstractLcnModuleSubHandler> metadataSubHandlers = new ArrayList<>();
|
||||
private final Map<ChannelUID, @Nullable Converter> converters = new HashMap<>();
|
||||
|
||||
static {
|
||||
VALUE_CONVERTERS.put("temperature", Converters.TEMPERATURE);
|
||||
VALUE_CONVERTERS.put("light", Converters.LIGHT);
|
||||
VALUE_CONVERTERS.put("co2", Converters.CO2);
|
||||
VALUE_CONVERTERS.put("current", Converters.CURRENT);
|
||||
VALUE_CONVERTERS.put("voltage", Converters.VOLTAGE);
|
||||
VALUE_CONVERTERS.put("angle", Converters.ANGLE);
|
||||
VALUE_CONVERTERS.put("windspeed", Converters.WINDSPEED);
|
||||
}
|
||||
|
||||
public LcnModuleHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
LcnModuleConfiguration localConfig = getConfigAs(LcnModuleConfiguration.class);
|
||||
LcnAddrMod localModuleAddress = moduleAddress = new LcnAddrMod(localConfig.segmentId, localConfig.moduleId);
|
||||
|
||||
try {
|
||||
// Determine serial number of manually added modules
|
||||
requestFirmwareVersionAndSerialNumberIfNotSet();
|
||||
|
||||
// create sub handlers
|
||||
ModInfo info = getPckGatewayHandler().getModInfo(localModuleAddress);
|
||||
for (LcnChannelGroup type : LcnChannelGroup.values()) {
|
||||
subHandlers.put(type, type.createSubHandler(this, info));
|
||||
}
|
||||
|
||||
// meta sub handlers, which are not assigned to a channel group
|
||||
metadataSubHandlers.add(new LcnModuleMetaAckSubHandler(this, info));
|
||||
metadataSubHandlers.add(new LcnModuleMetaFirmwareSubHandler(this, info));
|
||||
|
||||
// initialize converters
|
||||
for (Channel channel : thing.getChannels()) {
|
||||
Object unitObject = channel.getConfiguration().get("unit");
|
||||
Object parameterObject = channel.getConfiguration().get("parameter");
|
||||
Object invertConfig = channel.getConfiguration().get("invertState");
|
||||
|
||||
// Initialize value converters
|
||||
if (unitObject instanceof String) {
|
||||
switch ((String) unitObject) {
|
||||
case "power":
|
||||
case "energy":
|
||||
converters.put(channel.getUID(), new S0Converter(parameterObject));
|
||||
break;
|
||||
default:
|
||||
if (VALUE_CONVERTERS.containsKey(unitObject)) {
|
||||
converters.put(channel.getUID(), VALUE_CONVERTERS.get(unitObject));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize inversion converter
|
||||
if (invertConfig instanceof Boolean && invertConfig.equals(true)) {
|
||||
converters.put(channel.getUID(), INVERSION_CONVERTER);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// module is assumed as online, when the corresponding Bridge (PckGatewayHandler) is online.
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (LcnException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers requesting the firmware version of the LCN module. The message also contains the serial number.
|
||||
*
|
||||
* @throws LcnException when the handler is not initialized
|
||||
*/
|
||||
@SuppressWarnings("null")
|
||||
protected void requestFirmwareVersionAndSerialNumberIfNotSet() throws LcnException {
|
||||
String serialNumber = getThing().getProperties().get(Thing.PROPERTY_SERIAL_NUMBER);
|
||||
if (serialNumber == null || serialNumber.isEmpty()) {
|
||||
LcnAddrMod localModuleAddress = moduleAddress;
|
||||
if (localModuleAddress != null) {
|
||||
getPckGatewayHandler().getModInfo(localModuleAddress).requestFirmwareVersion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUid, Command command) {
|
||||
try {
|
||||
String groupId = channelUid.getGroupId();
|
||||
|
||||
if (!channelUid.isInGroup()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupId == null) {
|
||||
throw new LcnException("Group ID is null");
|
||||
}
|
||||
|
||||
LcnChannelGroup channelGroup = LcnChannelGroup.valueOf(groupId.toUpperCase());
|
||||
AbstractLcnModuleSubHandler subHandler = subHandlers.get(channelGroup);
|
||||
|
||||
if (subHandler == null) {
|
||||
throw new LcnException("Sub Handler not found for: " + channelGroup);
|
||||
}
|
||||
|
||||
Optional<Integer> number = channelUidToChannelNumber(channelUid, channelGroup);
|
||||
|
||||
if (command instanceof RefreshType) {
|
||||
number.ifPresent(n -> subHandler.handleRefresh(channelGroup, n));
|
||||
subHandler.handleRefresh(channelUid.getIdWithoutGroup());
|
||||
} else if (command instanceof OnOffType) {
|
||||
subHandler.handleCommandOnOff((OnOffType) command, channelGroup, number.get());
|
||||
} else if (command instanceof DimmerOutputCommand) {
|
||||
subHandler.handleCommandDimmerOutput((DimmerOutputCommand) command, number.get());
|
||||
} else if (command instanceof PercentType && number.isPresent()) {
|
||||
subHandler.handleCommandPercent((PercentType) command, channelGroup, number.get());
|
||||
} else if (command instanceof HSBType) {
|
||||
subHandler.handleCommandHsb((HSBType) command, channelUid.getIdWithoutGroup());
|
||||
} else if (command instanceof PercentType) {
|
||||
subHandler.handleCommandPercent((PercentType) command, channelGroup, channelUid.getIdWithoutGroup());
|
||||
} else if (command instanceof StringType) {
|
||||
subHandler.handleCommandString((StringType) command, number.get());
|
||||
} else if (command instanceof DecimalType) {
|
||||
DecimalType decimalType = (DecimalType) command;
|
||||
DecimalType nativeValue = getConverter(channelUid).onCommandFromItem(decimalType.doubleValue());
|
||||
subHandler.handleCommandDecimal(nativeValue, channelGroup, number.get());
|
||||
} else if (command instanceof QuantityType) {
|
||||
QuantityType<?> quantityType = (QuantityType<?>) command;
|
||||
DecimalType nativeValue = getConverter(channelUid).onCommandFromItem(quantityType);
|
||||
subHandler.handleCommandDecimal(nativeValue, channelGroup, number.get());
|
||||
} else if (command instanceof UpDownType) {
|
||||
Channel channel = thing.getChannel(channelUid);
|
||||
if (channel != null) {
|
||||
Object invertConfig = channel.getConfiguration().get("invertUpDown");
|
||||
boolean invertUpDown = invertConfig instanceof Boolean && (boolean) invertConfig;
|
||||
subHandler.handleCommandUpDown((UpDownType) command, channelGroup, number.get(), invertUpDown);
|
||||
}
|
||||
} else if (command instanceof StopMoveType) {
|
||||
subHandler.handleCommandStopMove((StopMoveType) command, channelGroup, number.get());
|
||||
} else {
|
||||
throw new LcnException("Unsupported command type");
|
||||
}
|
||||
} catch (IllegalArgumentException | NoSuchElementException | LcnException e) {
|
||||
logger.warn("{}: Failed to handle command {}: {}", channelUid, command.getClass().getSimpleName(),
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@NonNullByDefault({}) // getOrDefault()
|
||||
private Converter getConverter(ChannelUID channelUid) {
|
||||
return converters.getOrDefault(channelUid, Converters.IDENTITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a PCK messages arrives from the PCK gateway
|
||||
*
|
||||
* @param pck the message without line termination
|
||||
*/
|
||||
@SuppressWarnings("null")
|
||||
public void handleStatusMessage(String pck) {
|
||||
for (AbstractLcnModuleSubHandler handler : subHandlers.values()) {
|
||||
if (handler.tryParse(pck)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
metadataSubHandlers.forEach(h -> h.tryParse(pck));
|
||||
}
|
||||
|
||||
private Optional<Integer> channelUidToChannelNumber(ChannelUID channelUid, LcnChannelGroup channelGroup)
|
||||
throws LcnException {
|
||||
try {
|
||||
int number = Integer.parseInt(channelUid.getIdWithoutGroup()) - 1;
|
||||
|
||||
if (!channelGroup.isValidId(number)) {
|
||||
throw new LcnException("Out of range: " + number);
|
||||
}
|
||||
return Optional.of(number);
|
||||
} catch (NumberFormatException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private PckGatewayHandler getPckGatewayHandler() throws LcnException {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge == null) {
|
||||
throw new LcnException("No LCN-PCK gateway configured for this module");
|
||||
}
|
||||
|
||||
PckGatewayHandler handler = (PckGatewayHandler) bridge.getHandler();
|
||||
if (handler == null) {
|
||||
throw new LcnException("Could not get PckGatewayHandler");
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a PCK string for sending.
|
||||
*
|
||||
* @param command without the address part
|
||||
* @throws LcnException when the module address is unknown
|
||||
*/
|
||||
public void sendPck(String command) throws LcnException {
|
||||
getPckGatewayHandler().queue(getCommandAddress(), true, command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a PCK byte buffer for sending.
|
||||
*
|
||||
* @param command without the address part
|
||||
* @throws LcnException when the module address is unknown
|
||||
*/
|
||||
public void sendPck(byte[] command) throws LcnException {
|
||||
getPckGatewayHandler().queue(getCommandAddress(), true, command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the address, which shall be used when sending commands into the LCN bus. This can also be a group address.
|
||||
*
|
||||
* @return the address to send to
|
||||
* @throws LcnException when the address is unknown
|
||||
*/
|
||||
protected LcnAddr getCommandAddress() throws LcnException, LcnException {
|
||||
LcnAddr localAddress = moduleAddress;
|
||||
if (localAddress == null) {
|
||||
throw new LcnException("Module address not set");
|
||||
}
|
||||
return localAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when an update for this LCN module should be fired to openHAB.
|
||||
*
|
||||
* @param channelGroup the Channel to update
|
||||
* @param channelId the ID within the Channel to update
|
||||
* @param state the new state
|
||||
*/
|
||||
public void updateChannel(LcnChannelGroup channelGroup, String channelId, State state) {
|
||||
ChannelUID channelUid = createChannelUid(channelGroup, channelId);
|
||||
Converter converter = converters.get(channelUid);
|
||||
|
||||
State convertedState = state;
|
||||
if (converter != null) {
|
||||
convertedState = converter.onStateUpdateFromHandler(state);
|
||||
}
|
||||
|
||||
updateState(channelUid, convertedState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the LCN module's serial number property.
|
||||
*
|
||||
* @param serialNumber the new serial number
|
||||
*/
|
||||
public void updateSerialNumberProperty(String serialNumber) {
|
||||
updateProperty(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when an trigger for this LCN module should be fired to openHAB.
|
||||
*
|
||||
* @param channelGroup the Channel to update
|
||||
* @param channelId the ID within the Channel to update
|
||||
* @param event the event used to trigger
|
||||
*/
|
||||
public void triggerChannel(LcnChannelGroup channelGroup, String channelId, String event) {
|
||||
triggerChannel(createChannelUid(channelGroup, channelId), event);
|
||||
}
|
||||
|
||||
private ChannelUID createChannelUid(LcnChannelGroup channelGroup, String channelId) {
|
||||
return new ChannelUID(thing.getUID(), channelGroup.name().toLowerCase() + "#" + channelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the LCN module address against the own.
|
||||
*
|
||||
* @param physicalSegmentId which is 0 if it is the local segment
|
||||
* @param moduleId
|
||||
* @return true, if the given address matches the own address
|
||||
*/
|
||||
public boolean isMyAddress(String physicalSegmentId, String moduleId) {
|
||||
try {
|
||||
return new LcnAddrMod(getPckGatewayHandler().toLogicalSegmentId(Integer.parseInt(physicalSegmentId)),
|
||||
Integer.parseInt(moduleId)).equals(getStatusMessageAddress());
|
||||
} catch (LcnException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Collections.singleton(LcnModuleActions.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when an Ack from this module has been received.
|
||||
*/
|
||||
public void onAckRceived() {
|
||||
try {
|
||||
Connection connection = getPckGatewayHandler().getConnection();
|
||||
LcnAddrMod localModuleAddress = moduleAddress;
|
||||
if (connection != null && localModuleAddress != null) {
|
||||
getPckGatewayHandler().getModInfo(localModuleAddress).onAck(LcnBindingConstants.CODE_ACK, connection,
|
||||
getPckGatewayHandler().getTimeoutMs(), System.nanoTime());
|
||||
}
|
||||
} catch (LcnException e) {
|
||||
logger.warn("Connection or module address not set");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the address the handler shall react to, when a status message from this address is processed.
|
||||
*
|
||||
* @return the address for status messages
|
||||
*/
|
||||
public LcnAddrMod getStatusMessageAddress() {
|
||||
LcnAddrMod localmoduleAddress = moduleAddress;
|
||||
if (localmoduleAddress != null) {
|
||||
return localmoduleAddress;
|
||||
} else {
|
||||
return new LcnAddrMod(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
metadataSubHandlers.clear();
|
||||
subHandlers.clear();
|
||||
converters.clear();
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.thing.profiles.Profile;
|
||||
import org.openhab.core.thing.profiles.ProfileCallback;
|
||||
import org.openhab.core.thing.profiles.ProfileContext;
|
||||
import org.openhab.core.thing.profiles.ProfileFactory;
|
||||
import org.openhab.core.thing.profiles.ProfileType;
|
||||
import org.openhab.core.thing.profiles.ProfileTypeBuilder;
|
||||
import org.openhab.core.thing.profiles.ProfileTypeProvider;
|
||||
import org.openhab.core.thing.profiles.ProfileTypeUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Factory to create Profile instances. Also provides the available ProfileTypes and gives advise which profile to use
|
||||
* by a given link.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = { ProfileFactory.class, ProfileTypeProvider.class })
|
||||
public class LcnProfileFactory implements ProfileFactory, ProfileTypeProvider {
|
||||
private final Logger logger = LoggerFactory.getLogger(LcnProfileFactory.class);
|
||||
|
||||
@Override
|
||||
public Collection<ProfileTypeUID> getSupportedProfileTypeUIDs() {
|
||||
return Collections.singleton(DimmerOutputProfile.UID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ProfileType> getProfileTypes(@Nullable Locale locale) {
|
||||
return Collections.singleton(ProfileTypeBuilder.newState(DimmerOutputProfile.UID, "Dimmer Output (%)")
|
||||
.withSupportedItemTypes(CoreItemFactory.DIMMER, CoreItemFactory.COLOR)
|
||||
.withSupportedChannelTypeUIDs(
|
||||
new ChannelTypeUID(LcnBindingConstants.BINDING_ID, LcnChannelGroup.OUTPUT.name().toLowerCase()))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback,
|
||||
ProfileContext profileContext) {
|
||||
if (profileTypeUID.equals(DimmerOutputProfile.UID)) {
|
||||
return new DimmerOutputProfile(callback, profileContext);
|
||||
} else {
|
||||
logger.warn("Could not create {}", profileTypeUID);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link PckGatewayConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PckGatewayConfiguration {
|
||||
private @NonNullByDefault({}) String hostname;
|
||||
private int port;
|
||||
private @NonNullByDefault({}) String username;
|
||||
private @NonNullByDefault({}) String password;
|
||||
private @NonNullByDefault({}) String mode;
|
||||
private @NonNullByDefault({}) int timeoutMs;
|
||||
|
||||
public String getHostname() {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public String getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public int getTimeoutMs() {
|
||||
return timeoutMs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddr;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddrMod;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs.OutputPortDimMode;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.connection.Connection;
|
||||
import org.openhab.binding.lcn.internal.connection.ConnectionCallback;
|
||||
import org.openhab.binding.lcn.internal.connection.ConnectionSettings;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link PckGatewayHandler} is responsible for the communication via a PCK gateway.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PckGatewayHandler extends BaseBridgeHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(PckGatewayHandler.class);
|
||||
private @Nullable Connection connection;
|
||||
private Optional<Consumer<String>> pckListener = Optional.empty();
|
||||
private @Nullable PckGatewayConfiguration config;
|
||||
|
||||
public PckGatewayHandler(Bridge bridge) {
|
||||
super(bridge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void initialize() {
|
||||
PckGatewayConfiguration localConfig = config = getConfigAs(PckGatewayConfiguration.class);
|
||||
|
||||
String errorMessage = "Could not connect to LCN-PCHK/PKE: " + localConfig.getHostname() + ": ";
|
||||
|
||||
try {
|
||||
OutputPortDimMode dimMode;
|
||||
String mode = localConfig.getMode();
|
||||
if (LcnDefs.OutputPortDimMode.NATIVE50.name().equalsIgnoreCase(mode)) {
|
||||
dimMode = LcnDefs.OutputPortDimMode.NATIVE50;
|
||||
} else if (LcnDefs.OutputPortDimMode.NATIVE200.name().equalsIgnoreCase(mode)) {
|
||||
dimMode = LcnDefs.OutputPortDimMode.NATIVE200;
|
||||
} else {
|
||||
throw new LcnException("DimMode " + mode + " is not supported");
|
||||
}
|
||||
|
||||
ConnectionSettings settings = new ConnectionSettings("0", localConfig.getHostname(), localConfig.getPort(),
|
||||
localConfig.getUsername(), localConfig.getPassword(), dimMode, LcnDefs.OutputPortStatusMode.PERCENT,
|
||||
localConfig.getTimeoutMs());
|
||||
|
||||
connection = new Connection(settings, scheduler, new ConnectionCallback() {
|
||||
@Override
|
||||
public void onOnline() {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOffline(@Nullable String errorMessage) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage + ".");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPckMessageReceived(String message) {
|
||||
pckListener.ifPresent(l -> l.accept(message));
|
||||
getThing().getThings().stream().filter(t -> t.getStatus() == ThingStatus.ONLINE).map(t -> {
|
||||
LcnModuleHandler handler = (LcnModuleHandler) t.getHandler();
|
||||
if (handler == null) {
|
||||
logger.warn("Failed to process PCK message: Handler not set");
|
||||
}
|
||||
return handler;
|
||||
}).filter(h -> h != null).forEach(h -> h.handleStatusMessage(message));
|
||||
}
|
||||
});
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
} catch (LcnException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMessage + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Collections.singleton(LcnModuleDiscoveryService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
|
||||
if (childThing.getThingTypeUID().equals(LcnBindingConstants.THING_TYPE_MODULE)
|
||||
|| childThing.getThingTypeUID().equals(LcnBindingConstants.THING_TYPE_GROUP)) {
|
||||
try {
|
||||
LcnAddr addr = getLcnAddrFromThing(childThing);
|
||||
Connection localConnection = connection;
|
||||
if (localConnection != null) {
|
||||
localConnection.removeLcnModule(addr);
|
||||
}
|
||||
} catch (LcnException e) {
|
||||
logger.warn("Failed to read configuration: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private LcnAddr getLcnAddrFromThing(Thing childThing) throws LcnException {
|
||||
LcnModuleHandler lcnModuleHandler = (LcnModuleHandler) childThing.getHandler();
|
||||
if (lcnModuleHandler != null) {
|
||||
return lcnModuleHandler.getCommandAddress();
|
||||
} else {
|
||||
throw new LcnException("Could not get module handler");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a PCK (String) command to be sent to an LCN module.
|
||||
*
|
||||
* @param addr the modules address
|
||||
* @param wantsAck true, if the module shall send an ACK upon successful processing
|
||||
* @param pck the command to send
|
||||
*/
|
||||
public void queue(LcnAddr addr, boolean wantsAck, String pck) {
|
||||
Connection localConnection = connection;
|
||||
if (localConnection != null) {
|
||||
localConnection.queue(addr, wantsAck, pck);
|
||||
} else {
|
||||
logger.warn("Dropped PCK command: {}", pck);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a PCK (ByteBuffer) command to be sent to an LCN module.
|
||||
*
|
||||
* @param addr the modules address
|
||||
* @param wantsAck true, if the module shall send an ACK upon successful processing
|
||||
* @param pck the command to send
|
||||
*/
|
||||
public void queue(LcnAddr addr, boolean wantsAck, byte[] pck) {
|
||||
Connection localConnection = connection;
|
||||
if (localConnection != null) {
|
||||
localConnection.queue(addr, wantsAck, pck);
|
||||
} else {
|
||||
logger.warn("Dropped PCK command of length: {}", pck.length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a broadcast message to all LCN modules: All LCN modules are requested to answer with an Ack.
|
||||
*/
|
||||
void sendModuleDiscoveryCommand() {
|
||||
Connection localConnection = connection;
|
||||
if (localConnection != null) {
|
||||
localConnection.sendModuleDiscoveryCommand();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to an LCN module to respond with its serial number and firmware version.
|
||||
*
|
||||
* @param addr the module's address
|
||||
*/
|
||||
void sendSerialNumberRequest(LcnAddrMod addr) {
|
||||
Connection localConnection = connection;
|
||||
if (localConnection != null) {
|
||||
localConnection.sendSerialNumberRequest(addr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to an LCN module to respond with its configured name.
|
||||
*
|
||||
* @param addr the module's address
|
||||
*/
|
||||
void sendModuleNameRequest(LcnAddrMod addr) {
|
||||
Connection localConnection = connection;
|
||||
if (localConnection != null) {
|
||||
localConnection.sendModuleNameRequest(addr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ModInfo to a given module. Will be created if it doesn't exist,yet.
|
||||
*
|
||||
* @param addr the module's address
|
||||
* @return the ModInfo
|
||||
* @throws LcnException when this handler is not initialized, yet
|
||||
*/
|
||||
ModInfo getModInfo(LcnAddrMod addr) throws LcnException {
|
||||
Connection localConnection = connection;
|
||||
if (localConnection != null) {
|
||||
return localConnection.updateModuleData(addr);
|
||||
} else {
|
||||
throw new LcnException("Connection is null");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener to receive all PCK messages from this PCK gateway.
|
||||
*
|
||||
* @param listener the listener to add
|
||||
*/
|
||||
void registerPckListener(Consumer<String> listener) {
|
||||
this.pckListener = Optional.of(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all listeners for PCK messages from this PCK gateway.
|
||||
*/
|
||||
void removeAllPckListeners() {
|
||||
this.pckListener = Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Connection for this handler.
|
||||
*
|
||||
* @return the Connection
|
||||
*/
|
||||
@Nullable
|
||||
public Connection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the local segment ID. When no segments are used, the value is 0.
|
||||
*
|
||||
* @return the local segment ID
|
||||
*/
|
||||
public int getLocalSegmentId() {
|
||||
Connection localConnection = connection;
|
||||
if (localConnection != null) {
|
||||
return localConnection.getLocalSegId();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given physical segment ID (0 or 4 if local segment) to the logical segment ID (local segment ID).
|
||||
*
|
||||
* @param physicalSegmentId the segment ID to convert
|
||||
* @return the converted segment ID
|
||||
*/
|
||||
public int toLogicalSegmentId(int physicalSegmentId) {
|
||||
int localSegmentId = getLocalSegmentId();
|
||||
if ((physicalSegmentId == 0 || physicalSegmentId == 4) && localSegmentId != -1) {
|
||||
// PCK message came from local segment
|
||||
// physicalSegmentId == 0 => Module is programmed to send status messages to local segment only
|
||||
// physicalSegmentId == 4 => Module is programmed to send status messages globally (to all segments)
|
||||
// or segment coupler scan did not finish, yet (-1). Assume local segment, then.
|
||||
return localSegmentId;
|
||||
} else {
|
||||
return physicalSegmentId;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
Connection localConnection = connection;
|
||||
if (localConnection != null) {
|
||||
localConnection.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configured connection timeout for the PCK gateway.
|
||||
*
|
||||
* @return the timeout in ms
|
||||
*/
|
||||
public long getTimeoutMs() {
|
||||
PckGatewayConfiguration localConfig = config;
|
||||
return localConfig != null ? localConfig.getTimeoutMs() : 3500;
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.common;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
|
||||
/**
|
||||
* Holds the information to control dimmer outputs of an LCN module. Used when the user configured an "output" profile.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DimmerOutputCommand extends PercentType {
|
||||
private static final long serialVersionUID = 8147502412107723798L;
|
||||
private final boolean controlAllOutputs;
|
||||
private final boolean controlOutputs12;
|
||||
private final int rampMs;
|
||||
|
||||
public DimmerOutputCommand(BigDecimal value, boolean controlAllOutputs, boolean controlOutputs12, int rampMs) {
|
||||
super(value);
|
||||
this.controlAllOutputs = controlAllOutputs;
|
||||
this.controlOutputs12 = controlOutputs12;
|
||||
this.rampMs = rampMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ramp.
|
||||
*
|
||||
* @return ramp in milliseconds
|
||||
*/
|
||||
public int getRampMs() {
|
||||
return rampMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if all dimmer outputs shall be controlled.
|
||||
*
|
||||
* @return true, if all dimmer outputs shall be controlled
|
||||
*/
|
||||
public boolean isControlAllOutputs() {
|
||||
return controlAllOutputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if dimmer outputs 1+2 shall be controlled.
|
||||
*
|
||||
* @return true, if dimmer outputs 1+2 shall be controlled
|
||||
*/
|
||||
public boolean isControlOutputs12() {
|
||||
return controlOutputs12;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.common;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Represents an LCN address (module or group).
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class LcnAddr {
|
||||
/**
|
||||
* The logical segment ID. When no segments are used, the ID is always 0. When segments are used and the module is
|
||||
* in the local segment, the ID is the local's segment ID.
|
||||
*/
|
||||
protected final int segmentId;
|
||||
|
||||
/**
|
||||
* Constructs an address with a (logical) segment id.
|
||||
*
|
||||
* @param segId the segment id
|
||||
*/
|
||||
public LcnAddr(int segId) {
|
||||
this.segmentId = segId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the (logical) segment id.
|
||||
*
|
||||
* @return the segment id
|
||||
*/
|
||||
public int getSegmentId() {
|
||||
return this.segmentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the physical segment id ("local" segment replaced with 0).
|
||||
* Can be used to send data into the LCN bus.
|
||||
*
|
||||
* @param localSegegmentId the segment id of the local segment (managed by {@link Connection})
|
||||
* @return the physical segment id
|
||||
*/
|
||||
public int getPhysicalSegmentId(int localSegegmentId) {
|
||||
return this.segmentId == localSegegmentId ? 0 : this.segmentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the address against the LCN specification for valid addresses.
|
||||
*
|
||||
* @return true if address is valid
|
||||
*/
|
||||
public abstract boolean isValid();
|
||||
|
||||
/**
|
||||
* Queries the concrete address type.
|
||||
*
|
||||
* @return true if address is a group address (module address otherwise)
|
||||
*/
|
||||
public abstract boolean isGroup();
|
||||
|
||||
/**
|
||||
* Gets the address' module or group id (discarding the concrete type).
|
||||
*
|
||||
* @return the module or group id
|
||||
*/
|
||||
public abstract int getId();
|
||||
}
|
||||
@@ -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.lcn.internal.common;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Represents an LCN group address.
|
||||
* Can be used as a key in maps.
|
||||
* Hash codes are guaranteed to be unique as long as {@link #isValid()} is true.
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnAddrGrp extends LcnAddr implements Comparable<LcnAddrMod> {
|
||||
private final Logger logger = LoggerFactory.getLogger(LcnAddrGrp.class);
|
||||
private final int groupId;
|
||||
|
||||
/**
|
||||
* Constructs a group address with (logical) segment id and group id.
|
||||
*
|
||||
* @param segId the segment id
|
||||
* @param grpId the group id
|
||||
*/
|
||||
public LcnAddrGrp(int segId, int grpId) {
|
||||
super(segId);
|
||||
this.groupId = grpId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the group id.
|
||||
*
|
||||
* @return the group id
|
||||
*/
|
||||
public int getGroupId() {
|
||||
return this.groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
// segId:
|
||||
// 0 = Local, 1..2 = Not allowed (but "seen in the wild")
|
||||
// 3 = Broadcast, 4 = Status messages, 5..127, 128 = Segment-bus disabled (valid value)
|
||||
// grpId:
|
||||
// 3 = Broadcast, 4 = Status messages, 5..254
|
||||
return this.segmentId >= 0 && this.segmentId <= 128 && this.groupId >= 3 && this.groupId <= 254;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroup() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return this.groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Reversing the bits helps to generate better balanced trees as ids tend to be "user-sorted"
|
||||
try {
|
||||
if (this.isValid()) {
|
||||
return ReverseNumber.reverseUInt8(this.groupId) << 8 + ReverseNumber.reverseUInt8(this.segmentId);
|
||||
}
|
||||
} catch (LcnException ex) {
|
||||
logger.warn("Could not calculate hash code");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof LcnAddrGrp)) {
|
||||
return false;
|
||||
}
|
||||
return this.segmentId == ((LcnAddrGrp) obj).segmentId && this.groupId == ((LcnAddrGrp) obj).groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(LcnAddrMod other) {
|
||||
return this.hashCode() - other.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.isValid() ? String.format("S%03dG%03d", this.segmentId, this.groupId) : "Invalid";
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.common;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Represents an LCN module address.
|
||||
* Can be used as a key in maps.
|
||||
* Hash codes are guaranteed to be unique as long as {@link #isValid()} is true.
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnAddrMod extends LcnAddr implements Comparable<LcnAddrMod> {
|
||||
private final Logger logger = LoggerFactory.getLogger(LcnAddrMod.class);
|
||||
private final int moduleId;
|
||||
|
||||
/**
|
||||
* Constructs a module address with (logical) segment id and module id.
|
||||
*
|
||||
* @param segId the segment id
|
||||
* @param modId the module id
|
||||
*/
|
||||
public LcnAddrMod(int segId, int modId) {
|
||||
super(segId);
|
||||
this.moduleId = modId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the module id.
|
||||
*
|
||||
* @return the module id
|
||||
*/
|
||||
public int getModuleId() {
|
||||
return this.moduleId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
// segId:
|
||||
// 0 = Local, 1..2 = Not allowed (but "seen in the wild")
|
||||
// 3 = Broadcast, 4 = Status messages, 5..127, 128 = Segment-bus disabled (valid value)
|
||||
// modId:
|
||||
// 1 = LCN-PRO, 2 = LCN-GVS/LCN-W, 4 = PCHK, 5..254, 255 = Unprog. (valid, but irrelevant here)
|
||||
return this.segmentId >= 0 && this.segmentId <= 128 && this.moduleId >= 1 && this.moduleId <= 254;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return this.moduleId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Reversing the bits helps to generate better balanced trees as ids tend to be "user-sorted"
|
||||
try {
|
||||
if (this.isValid()) {
|
||||
return ReverseNumber.reverseUInt8(this.moduleId) << 8 + ReverseNumber.reverseUInt8(this.segmentId);
|
||||
}
|
||||
} catch (LcnException ex) {
|
||||
logger.warn("Could not calculate hash code");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof LcnAddrMod)) {
|
||||
return false;
|
||||
}
|
||||
return this.segmentId == ((LcnAddrMod) obj).segmentId && this.moduleId == ((LcnAddrMod) obj).moduleId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(LcnAddrMod other) {
|
||||
return this.hashCode() - other.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.isValid() ? String.format("S%03dM%03d", this.segmentId, this.moduleId) : "Invalid";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.common;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.binding.lcn.internal.subhandler.AbstractLcnModuleSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleBinarySensorSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleCodeSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleHostCommandSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleKeyLockTableSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleLedSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleLogicSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleOutputSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleRelaySubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleRollershutterOutputSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleRollershutterRelaySubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleRvarLockSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleRvarSetpointSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleS0CounterSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleThresholdSubHandler;
|
||||
import org.openhab.binding.lcn.internal.subhandler.LcnModuleVariableSubHandler;
|
||||
|
||||
/**
|
||||
* Defines the supported channels of an LCN module handler.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum LcnChannelGroup {
|
||||
OUTPUT(4, LcnModuleOutputSubHandler::new),
|
||||
ROLLERSHUTTEROUTPUT(1, LcnModuleRollershutterOutputSubHandler::new),
|
||||
RELAY(8, LcnModuleRelaySubHandler::new),
|
||||
ROLLERSHUTTERRELAY(4, LcnModuleRollershutterRelaySubHandler::new),
|
||||
LED(12, LcnModuleLedSubHandler::new),
|
||||
LOGIC(4, LcnModuleLogicSubHandler::new),
|
||||
BINARYSENSOR(8, LcnModuleBinarySensorSubHandler::new),
|
||||
VARIABLE(12, LcnModuleVariableSubHandler::new),
|
||||
RVARSETPOINT(2, LcnModuleRvarSetpointSubHandler::new),
|
||||
RVARLOCK(2, LcnModuleRvarLockSubHandler::new),
|
||||
THRESHOLDREGISTER1(5, LcnModuleThresholdSubHandler::new),
|
||||
THRESHOLDREGISTER2(4, LcnModuleThresholdSubHandler::new),
|
||||
THRESHOLDREGISTER3(4, LcnModuleThresholdSubHandler::new),
|
||||
THRESHOLDREGISTER4(4, LcnModuleThresholdSubHandler::new),
|
||||
S0INPUT(4, LcnModuleS0CounterSubHandler::new),
|
||||
KEYLOCKTABLEA(8, LcnModuleKeyLockTableSubHandler::new),
|
||||
KEYLOCKTABLEB(8, LcnModuleKeyLockTableSubHandler::new),
|
||||
KEYLOCKTABLEC(8, LcnModuleKeyLockTableSubHandler::new),
|
||||
KEYLOCKTABLED(8, LcnModuleKeyLockTableSubHandler::new),
|
||||
CODE(0, LcnModuleCodeSubHandler::new),
|
||||
HOSTCOMMAND(0, LcnModuleHostCommandSubHandler::new);
|
||||
|
||||
private int count;
|
||||
private BiFunction<LcnModuleHandler, ModInfo, ? extends AbstractLcnModuleSubHandler> handlerFactory;
|
||||
|
||||
private LcnChannelGroup(int count,
|
||||
BiFunction<LcnModuleHandler, ModInfo, ? extends AbstractLcnModuleSubHandler> handlerFactory) {
|
||||
this.count = count;
|
||||
this.handlerFactory = handlerFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of Channels within the channel group.
|
||||
*
|
||||
* @return the Channel count
|
||||
*/
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given Channel id against the max. Channel count in this Channel group.
|
||||
*
|
||||
* @param number the number to check
|
||||
* @return true, if the number is in the range
|
||||
*/
|
||||
public boolean isValidId(int number) {
|
||||
return number >= 0 && number < count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sub handler class to handle this Channel group.
|
||||
*
|
||||
* @return the sub handler class
|
||||
*/
|
||||
public AbstractLcnModuleSubHandler createSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
return handlerFactory.apply(handler, info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a given table ID into the corresponding Channel group.
|
||||
*
|
||||
* @param tableId to convert
|
||||
* @return the channel group
|
||||
* @throws LcnException when the ID is out of range
|
||||
*/
|
||||
public static LcnChannelGroup fromTableId(int tableId) throws LcnException {
|
||||
switch (tableId) {
|
||||
case 0:
|
||||
return KEYLOCKTABLEA;
|
||||
case 1:
|
||||
return KEYLOCKTABLEB;
|
||||
case 2:
|
||||
return KEYLOCKTABLEC;
|
||||
case 3:
|
||||
return KEYLOCKTABLED;
|
||||
default:
|
||||
throw new LcnException("Unknown key table ID: " + tableId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.common;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Common definitions and helpers for the PCK protocol.
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
* @author Fabian Wolter - Migration to OH2
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class LcnDefs {
|
||||
/** Text encoding used by LCN-PCHK. */
|
||||
public static final Charset LCN_ENCODING = StandardCharsets.UTF_8;
|
||||
/** Number of thresholds registers of an LCN module */
|
||||
public static final int THRESHOLD_REGISTER_COUNT = 4;
|
||||
/** Number of key tables of an LCN module. */
|
||||
public static final int KEY_TABLE_COUNT = 4;
|
||||
/** Number of key tables of an LCN module before firmware 0C030C0. */
|
||||
public static final int KEY_TABLE_COUNT_UNTIL_0C030C0 = 3;
|
||||
/** Number of keys per table of an LCN module */
|
||||
public static final int KEY_COUNT = 8;
|
||||
/** Number of thresholds before LCN module firmware version 2013 */
|
||||
public static final int THRESHOLD_COUNT_BEFORE_2013 = 5;
|
||||
/**
|
||||
* Default dimmer output ramp when used with roller shutters. Results in a switching delay of 600ms. Value copied
|
||||
* from the LCN-PRO motor/shutter command dialog.
|
||||
*/
|
||||
public static final int ROLLER_SHUTTER_RAMP_MS = 4000;
|
||||
/** Max. value of a variable, threshold or regulator setpoint */
|
||||
public static final int MAX_VARIABLE_VALUE = 32768;
|
||||
/** The fixed ramp when output 1+2 are controlled */
|
||||
public static final int FIXED_RAMP_MS = 250;
|
||||
/** Authentication at LCN-PCHK: Request user name. */
|
||||
public static final String AUTH_USERNAME = "Username:";
|
||||
/** Authentication at LCN-PCHK: Request password. */
|
||||
public static final String AUTH_PASSWORD = "Password:";
|
||||
/** LCN-PK/PKU is connected. */
|
||||
public static final String LCNCONNSTATE_CONNECTED = "$io:#LCN:connected";
|
||||
/** LCN-PK/PKU is disconnected. */
|
||||
public static final String LCNCONNSTATE_DISCONNECTED = "$io:#LCN:disconnected";
|
||||
/** LCN-PCHK/PKE has not enough licenses to handle this connection. */
|
||||
public static final String INSUFFICIENT_LICENSES = "$err:(license?)";
|
||||
|
||||
/**
|
||||
* LCN dimming mode.
|
||||
* If solely modules with firmware 170206 or newer are present, LCN-PRO automatically programs {@link #NATIVE200}.
|
||||
* Otherwise the default is {@link #NATIVE50}.
|
||||
* Since LCN-PCHK doesn't know the current mode, it must explicitly be set.
|
||||
*/
|
||||
public enum OutputPortDimMode {
|
||||
NATIVE50, // 0..50 dimming steps (all LCN module generations)
|
||||
NATIVE200 // 0..200 dimming steps (since 170206)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells LCN-PCHK how to format output-port status-messages.
|
||||
* {@link #NATIVE} allows to show the status in half-percent steps (e.g. "10.5").
|
||||
* {@link #NATIVE} is completely backward compatible and there are no restrictions
|
||||
* concerning the LCN module generations. It requires LCN-PCHK 2.3 or higher though.
|
||||
*/
|
||||
public enum OutputPortStatusMode {
|
||||
PERCENT, // Default (compatible with all versions of LCN-PCHK)
|
||||
NATIVE // 0..200 steps (since LCN-PCHK 2.3)
|
||||
}
|
||||
|
||||
/** Possible states for LCN LEDs. */
|
||||
public enum LedStatus {
|
||||
OFF,
|
||||
ON,
|
||||
BLINK,
|
||||
FLICKER;
|
||||
}
|
||||
|
||||
/** Possible states for LCN logic-operations. */
|
||||
public enum LogicOpStatus {
|
||||
NOT,
|
||||
OR, // Note: Actually not correct since AND won't be OR also
|
||||
AND;
|
||||
}
|
||||
|
||||
/** Time units used for several LCN commands. */
|
||||
public enum TimeUnit {
|
||||
SECONDS,
|
||||
MINUTES,
|
||||
HOURS,
|
||||
DAYS;
|
||||
}
|
||||
|
||||
/** Relay-state modifiers used in LCN commands. */
|
||||
public enum RelayStateModifier {
|
||||
ON,
|
||||
OFF,
|
||||
TOGGLE,
|
||||
NOCHANGE
|
||||
}
|
||||
|
||||
/** Value-reference for relative LCN variable commands. */
|
||||
public enum RelVarRef {
|
||||
CURRENT,
|
||||
PROG // Programmed value (LCN-PRO). Relevant for set-points and thresholds.
|
||||
}
|
||||
|
||||
/** Command types used when sending LCN keys. */
|
||||
public enum SendKeyCommand {
|
||||
DONTSEND(0),
|
||||
HIT(1),
|
||||
MAKE(2),
|
||||
BREAK(3);
|
||||
|
||||
private int id;
|
||||
|
||||
SendKeyCommand(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public static SendKeyCommand get(int id) {
|
||||
return Arrays.stream(values()).filter(v -> v.getId() == id).findAny()
|
||||
.orElseThrow(NoSuchElementException::new);
|
||||
}
|
||||
}
|
||||
|
||||
/** Key-lock modifiers used in LCN commands. */
|
||||
public enum KeyLockStateModifier {
|
||||
ON,
|
||||
OFF,
|
||||
TOGGLE,
|
||||
NOCHANGE
|
||||
}
|
||||
|
||||
/** List of key tables of an LCN module */
|
||||
public enum KeyTable {
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of booleans from an input integer (actually a byte).
|
||||
*
|
||||
* @param input the input byte (0..255)
|
||||
* @return the array of 8 booleans
|
||||
* @throws IllegalArgumentException if input is out of range (not a byte)
|
||||
*/
|
||||
public static boolean[] getBooleanValue(int inputByte) throws IllegalArgumentException {
|
||||
if (inputByte < 0 || inputByte > 255) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
boolean[] result = new boolean[8];
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
result[i] = (inputByte & (1 << i)) != 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.common;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Default checked exception.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnException extends Exception {
|
||||
private static final long serialVersionUID = -4341882774124288028L;
|
||||
|
||||
public LcnException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public LcnException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public LcnException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,823 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.common;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Helpers to generate LCN-PCK commands.
|
||||
* <p>
|
||||
* LCN-PCK is the command-syntax used by LCN-PCHK to send and receive LCN commands.
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
* @author Fabian Wolter - Migration to OH2
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class PckGenerator {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PckGenerator.class);
|
||||
/** Termination character after a PCK message */
|
||||
public static final String TERMINATION = "\n";
|
||||
|
||||
/**
|
||||
* Generates a keep-alive.
|
||||
* LCN-PCHK will close the connection if it does not receive any commands from
|
||||
* an open {@link Connection} for a specific period (10 minutes by default).
|
||||
*
|
||||
* @param counter the current ping's id (optional, but "best practice"). Should start with 1
|
||||
* @return the PCK command as text
|
||||
*/
|
||||
public static String ping(int counter) {
|
||||
return String.format("^ping%d", counter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PCK command that will set the LCN-PCHK connection's operation mode.
|
||||
* This influences how output-port commands and status are interpreted and must be
|
||||
* in sync with the LCN bus.
|
||||
*
|
||||
* @param dimMode see {@link LcnDefs.OutputPortDimMode}
|
||||
* @param statusMode see {@link LcnDefs.OutputPortStatusMode}
|
||||
* @return the PCK command as text
|
||||
*/
|
||||
public static String setOperationMode(LcnDefs.OutputPortDimMode dimMode, LcnDefs.OutputPortStatusMode statusMode) {
|
||||
return "!OM" + (dimMode == LcnDefs.OutputPortDimMode.NATIVE200 ? "1" : "0")
|
||||
+ (statusMode == LcnDefs.OutputPortStatusMode.PERCENT ? "P" : "N");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PCK address header.
|
||||
* Used for commands to LCN modules and groups.
|
||||
*
|
||||
* @param addr the target's address (module or group)
|
||||
* @param localSegId the local segment id where the physical bus connection is located
|
||||
* @param wantsAck true to claim an acknowledge / receipt from the target
|
||||
* @return the PCK address header as text
|
||||
*/
|
||||
public static String generateAddressHeader(LcnAddr addr, int localSegId, boolean wantsAck) {
|
||||
return String.format(">%s%03d%03d%s", addr.isGroup() ? "G" : "M", addr.getPhysicalSegmentId(localSegId),
|
||||
addr.getId(), wantsAck ? "!" : ".");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a scan-command for LCN segment-couplers.
|
||||
* Used to detect the local segment (where the physical bus connection is located).
|
||||
*
|
||||
* @return the PCK command (without address header) as text
|
||||
*/
|
||||
public static String segmentCouplerScan() {
|
||||
return "SK";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a firmware/serial-number request.
|
||||
*
|
||||
* @return the PCK command (without address header) as text
|
||||
*/
|
||||
public static String requestSn() {
|
||||
return "SN";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command to request a part of a name of a module.
|
||||
*
|
||||
* @param partNumber 0..1
|
||||
* @return the PCK command (without address header) as text
|
||||
*/
|
||||
public static String requestModuleName(int partNumber) {
|
||||
return "NMN" + (partNumber + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an output-port status request.
|
||||
*
|
||||
* @param outputId 0..3
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static String requestOutputStatus(int outputId) throws LcnException {
|
||||
if (outputId < 0 || outputId > 3) {
|
||||
throw new LcnException();
|
||||
}
|
||||
return String.format("SMA%d", outputId + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a dim command for a single output-port.
|
||||
*
|
||||
* @param outputId 0..3
|
||||
* @param percent 0..100
|
||||
* @param rampMs ramp in milliseconds
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static String dimOutput(int outputId, double percent, int rampMs) throws LcnException {
|
||||
if (outputId < 0 || outputId > 3) {
|
||||
throw new LcnException();
|
||||
}
|
||||
int rampNative = PckGenerator.timeToRampValue(rampMs);
|
||||
int n = (int) Math.round(percent * 2);
|
||||
if ((n % 2) == 0) { // Use the percent command (supported by all LCN-PCHK versions)
|
||||
return String.format("A%dDI%03d%03d", outputId + 1, n / 2, rampNative);
|
||||
} else { // We have a ".5" value. Use the native command (supported since LCN-PCHK 2.3)
|
||||
return String.format("O%dDI%03d%03d", outputId + 1, n, rampNative);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a dim command for all output-ports.
|
||||
*
|
||||
* Attention: This command is supported since module firmware version 180501 AND LCN-PCHK 2.61
|
||||
*
|
||||
* @param firstPercent dimmer value of the first output 0..100
|
||||
* @param secondPercent dimmer value of the first output 0..100
|
||||
* @param thirdPercent dimmer value of the first output 0..100
|
||||
* @param fourthPercent dimmer value of the first output 0..100
|
||||
* @param rampMs ramp in milliseconds
|
||||
* @return the PCK command (without address header) as text
|
||||
*/
|
||||
public static String dimAllOutputs(double firstPercent, double secondPercent, double thirdPercent,
|
||||
double fourthPercent, int rampMs) {
|
||||
long n1 = Math.round(firstPercent * 2);
|
||||
long n2 = Math.round(secondPercent * 2);
|
||||
long n3 = Math.round(thirdPercent * 2);
|
||||
long n4 = Math.round(fourthPercent * 2);
|
||||
|
||||
return String.format("OY%03d%03d%03d%03d%03d", n1, n2, n3, n4, timeToRampValue(rampMs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a control command for switching all outputs ON or OFF with a fixed ramp of 0.5s.
|
||||
*
|
||||
* @param percent 0..100
|
||||
* @returnthe PCK command (without address header) as text
|
||||
*/
|
||||
public static String controlAllOutputs(double percent) {
|
||||
return String.format("AH%03d", Math.round(percent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a control command for switching dimmer output 1 and 2 both ON or OFF with a fixed ramp of 0.5s or
|
||||
* without ramp.
|
||||
*
|
||||
* @param on true, if outputs shall be switched on
|
||||
* @param ramp true, if the ramp shall be 0.5s, else 0s
|
||||
* @return the PCK command (without address header) as text
|
||||
*/
|
||||
public static String controlOutputs12(boolean on, boolean ramp) {
|
||||
int commandByte;
|
||||
if (on) {
|
||||
commandByte = ramp ? 0xC8 : 0xFD;
|
||||
} else {
|
||||
commandByte = ramp ? 0x00 : 0xFC;
|
||||
}
|
||||
return String.format("X2%03d%03d%03d", 1, commandByte, commandByte);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a dim command for setting the brightness of dimmer output 1 and 2 with a fixed ramp of 0.5s.
|
||||
*
|
||||
* @param percent brightness of both outputs 0..100
|
||||
* @return the PCK command (without address header) as text
|
||||
*/
|
||||
public static String dimOutputs12(double percent) {
|
||||
long localPercent = Math.round(percent);
|
||||
return String.format("AY%03d%03d", localPercent, localPercent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Let an output flicker.
|
||||
*
|
||||
* @param outputId output id 0..3
|
||||
* @param depth flicker depth, the higher the deeper 0..2
|
||||
* @param ramp the flicker speed 0..2
|
||||
* @param count number of flashes 1..15
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException when the input values are out of range
|
||||
*/
|
||||
public static String flickerOutput(int outputId, int depth, int ramp, int count) throws LcnException {
|
||||
if (outputId < 0 || outputId > 3) {
|
||||
throw new LcnException("Output number out of range");
|
||||
}
|
||||
if (count < 1 || count > 15) {
|
||||
throw new LcnException("Number of flashes out of range");
|
||||
}
|
||||
String depthString;
|
||||
switch (depth) {
|
||||
case 0:
|
||||
depthString = "G";
|
||||
break;
|
||||
case 1:
|
||||
depthString = "M";
|
||||
break;
|
||||
case 2:
|
||||
depthString = "S";
|
||||
break;
|
||||
default:
|
||||
throw new LcnException("Depth out of range");
|
||||
}
|
||||
String rampString;
|
||||
switch (ramp) {
|
||||
case 0:
|
||||
rampString = "L";
|
||||
break;
|
||||
case 1:
|
||||
rampString = "M";
|
||||
break;
|
||||
case 2:
|
||||
rampString = "S";
|
||||
break;
|
||||
default:
|
||||
throw new LcnException("Ramp out of range");
|
||||
}
|
||||
return String.format("A%dFL%s%s%02d", outputId + 1, depthString, rampString, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command to change the value of an output-port.
|
||||
*
|
||||
* @param outputId 0..3
|
||||
* @param percent -100..100
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static String relOutput(int outputId, double percent) throws LcnException {
|
||||
if (outputId < 0 || outputId > 3) {
|
||||
throw new LcnException();
|
||||
}
|
||||
int n = (int) Math.round(percent * 2);
|
||||
if ((n % 2) == 0) { // Use the percent command (supported by all LCN-PCHK versions)
|
||||
return String.format("A%d%s%03d", outputId + 1, percent >= 0 ? "AD" : "SB", Math.abs(n / 2));
|
||||
} else { // We have a ".5" value. Use the native command (supported since LCN-PCHK 2.3)
|
||||
return String.format("O%d%s%03d", outputId + 1, percent >= 0 ? "AD" : "SB", Math.abs(n));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command that toggles a single output-port (on->off, off->on).
|
||||
*
|
||||
* @param outputId 0..3
|
||||
* @param ramp see {@link PckGenerator#timeToRampValue(int)}
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static String toggleOutput(int outputId, int ramp) throws LcnException {
|
||||
if (outputId < 0 || outputId > 3) {
|
||||
throw new LcnException();
|
||||
}
|
||||
return String.format("A%dTA%03d", outputId + 1, ramp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command that toggles all output-ports (on->off, off->on).
|
||||
*
|
||||
* @param ramp see {@link PckGenerator#timeToRampValue(int)}
|
||||
* @return the PCK command (without address header) as text
|
||||
*/
|
||||
public static String toggleAllOutputs(int ramp) {
|
||||
return String.format("AU%03d", ramp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a relays-status request.
|
||||
*
|
||||
* @return the PCK command (without address header) as text
|
||||
*/
|
||||
public static String requestRelaysStatus() {
|
||||
return "SMR";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command to control relays.
|
||||
*
|
||||
* @param states the 8 modifiers for the relay states
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static String controlRelays(LcnDefs.RelayStateModifier[] states) throws LcnException {
|
||||
if (states.length != 8) {
|
||||
throw new LcnException();
|
||||
}
|
||||
StringBuilder ret = new StringBuilder("R8");
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
switch (states[i]) {
|
||||
case ON:
|
||||
ret.append("1");
|
||||
break;
|
||||
case OFF:
|
||||
ret.append("0");
|
||||
break;
|
||||
case TOGGLE:
|
||||
ret.append("U");
|
||||
break;
|
||||
case NOCHANGE:
|
||||
ret.append("-");
|
||||
break;
|
||||
default:
|
||||
throw new LcnException();
|
||||
}
|
||||
}
|
||||
return ret.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a binary-sensors status request.
|
||||
*
|
||||
* @return the PCK command (without address header) as text
|
||||
*/
|
||||
public static String requestBinSensorsStatus() {
|
||||
return "SMB";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command that sets a variable absolute.
|
||||
*
|
||||
* @param number regulator number 0..1
|
||||
* @param value the absolute value to set
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException
|
||||
*/
|
||||
public static String setSetpointAbsolute(int number, int value) {
|
||||
int internalValue = value;
|
||||
// Set absolute (not in PCK yet)
|
||||
int b1 = number << 6; // 01000000
|
||||
b1 |= 0x20; // xx10xxxx (set absolute)
|
||||
if (value < 1000) {
|
||||
internalValue = 1000 - internalValue;
|
||||
b1 |= 8;
|
||||
} else {
|
||||
internalValue -= 1000;
|
||||
}
|
||||
b1 |= (internalValue >> 8) & 0x0f; // xxxx1111
|
||||
int b2 = internalValue & 0xff;
|
||||
return String.format("X2%03d%03d%03d", 30, b1, b2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command to change the value of a variable.
|
||||
*
|
||||
* @param variable the target variable to change
|
||||
* @param type the reference-point
|
||||
* @param value the native LCN value to add/subtract (can be negative)
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if command is not supported
|
||||
*/
|
||||
public static String setVariableRelative(Variable variable, LcnDefs.RelVarRef type, int value) {
|
||||
if (variable.getNumber() == 0) {
|
||||
// Old command for variable 1 / T-var (compatible with all modules)
|
||||
return String.format("Z%s%d", value >= 0 ? "A" : "S", Math.abs(value));
|
||||
} else { // New command for variable 1-12 (compatible with all modules, since LCN-PCHK 2.8)
|
||||
return String.format("Z%s%03d%d", value >= 0 ? "+" : "-", variable.getNumber() + 1, Math.abs(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command the change the value of a regulator setpoint relative.
|
||||
*
|
||||
* @param number 0..1
|
||||
* @param type relative to the current or to the programmed value
|
||||
* @param value the relative value -4000..+4000
|
||||
* @return the PCK command (without address header) as text
|
||||
*/
|
||||
public static String setSetpointRelative(int number, LcnDefs.RelVarRef type, int value) {
|
||||
return String.format("RE%sS%s%s%d", number == 0 ? "A" : "B", type == LcnDefs.RelVarRef.CURRENT ? "A" : "P",
|
||||
value >= 0 ? "+" : "-", Math.abs(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command the change the value of a threshold relative.
|
||||
*
|
||||
* @param variable the threshold to change
|
||||
* @param type relative to the current or to the programmed value
|
||||
* @param value the relative value -4000..+4000
|
||||
* @param is2013 true, if the LCN module's firmware is equal to or newer than 2013
|
||||
* @return the PCK command (without address header) as text
|
||||
*/
|
||||
public static String setThresholdRelative(Variable variable, LcnDefs.RelVarRef type, int value, boolean is2013)
|
||||
throws LcnException {
|
||||
if (is2013) { // New command for registers 1-4 (since 170206, LCN-PCHK 2.8)
|
||||
return String.format("SS%s%04d%sR%d%d", type == LcnDefs.RelVarRef.CURRENT ? "R" : "E", Math.abs(value),
|
||||
value >= 0 ? "A" : "S", variable.getNumber() + 1, variable.getThresholdNumber().get() + 1);
|
||||
} else if (variable.getNumber() == 0) { // Old command for register 1 (before 170206)
|
||||
return String.format("SS%s%04d%s%s%s%s%s%s", type == LcnDefs.RelVarRef.CURRENT ? "R" : "E", Math.abs(value),
|
||||
value >= 0 ? "A" : "S", variable.getThresholdNumber().get() == 0 ? "1" : "0",
|
||||
variable.getThresholdNumber().get() == 1 ? "1" : "0",
|
||||
variable.getThresholdNumber().get() == 2 ? "1" : "0",
|
||||
variable.getThresholdNumber().get() == 3 ? "1" : "0",
|
||||
variable.getThresholdNumber().get() == 4 ? "1" : "0");
|
||||
} else {
|
||||
throw new LcnException(
|
||||
"Module does not have threshold register " + (variable.getThresholdNumber().get() + 1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a variable value request.
|
||||
*
|
||||
* @param variable the variable to request
|
||||
* @param firmwareVersion the target module's firmware version
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if command is not supported
|
||||
*/
|
||||
public static String requestVarStatus(Variable variable, int firmwareVersion) throws LcnException {
|
||||
if (firmwareVersion >= LcnBindingConstants.FIRMWARE_2013) {
|
||||
int id = variable.getNumber();
|
||||
switch (variable.getType()) {
|
||||
case UNKNOWN:
|
||||
throw new LcnException("Variable unknown");
|
||||
case VARIABLE:
|
||||
return String.format("MWT%03d", id + 1);
|
||||
case REGULATOR:
|
||||
return String.format("MWS%03d", id + 1);
|
||||
case THRESHOLD:
|
||||
return String.format("SE%03d", id + 1); // Whole register
|
||||
case S0INPUT:
|
||||
return String.format("MWC%03d", id + 1);
|
||||
}
|
||||
throw new LcnException("Unsupported variable type: " + variable);
|
||||
} else {
|
||||
switch (variable) {
|
||||
case VARIABLE1:
|
||||
return "MWV";
|
||||
case VARIABLE2:
|
||||
return "MWTA";
|
||||
case VARIABLE3:
|
||||
return "MWTB";
|
||||
case RVARSETPOINT1:
|
||||
return "MWSA";
|
||||
case RVARSETPOINT2:
|
||||
return "MWSB";
|
||||
case THRESHOLDREGISTER11:
|
||||
case THRESHOLDREGISTER12:
|
||||
case THRESHOLDREGISTER13:
|
||||
case THRESHOLDREGISTER14:
|
||||
case THRESHOLDREGISTER15:
|
||||
return "SL1"; // Whole register
|
||||
default:
|
||||
throw new LcnException("Unsupported variable type: " + variable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a request for LED and logic-operations states.
|
||||
*
|
||||
* @return the PCK command (without address header) as text
|
||||
*/
|
||||
public static String requestLedsAndLogicOpsStatus() {
|
||||
return "SMT";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command to the set the state of a single LED.
|
||||
*
|
||||
* @param ledId 0..11
|
||||
* @param state the state to set
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static String controlLed(int ledId, LcnDefs.LedStatus state) throws LcnException {
|
||||
if (ledId < 0 || ledId > 11) {
|
||||
throw new LcnException();
|
||||
}
|
||||
return String.format("LA%03d%s", ledId + 1, state == LcnDefs.LedStatus.OFF ? "A"
|
||||
: state == LcnDefs.LedStatus.ON ? "E" : state == LcnDefs.LedStatus.BLINK ? "B" : "F");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command to send LCN keys.
|
||||
*
|
||||
* @param cmds the 4 concrete commands to send for the tables (A-D)
|
||||
* @param keys the tables' 8 key-states (true means "send")
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static String sendKeys(LcnDefs.SendKeyCommand[] cmds, boolean[] keys) throws LcnException {
|
||||
if (cmds.length != 4 || keys.length != 8) {
|
||||
throw new LcnException();
|
||||
}
|
||||
StringBuilder ret = new StringBuilder("TS");
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
switch (cmds[i]) {
|
||||
case HIT:
|
||||
ret.append("K");
|
||||
break;
|
||||
case MAKE:
|
||||
ret.append("L");
|
||||
break;
|
||||
case BREAK:
|
||||
ret.append("O");
|
||||
break;
|
||||
case DONTSEND:
|
||||
// By skipping table D (if it is not used), we use the old command
|
||||
// for table A-C which is compatible with older LCN modules
|
||||
if (i < 3) {
|
||||
ret.append("-");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new LcnException();
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
ret.append(keys[i] ? "1" : "0");
|
||||
}
|
||||
return ret.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command to send LCN keys deferred / delayed.
|
||||
*
|
||||
* @param tableId 0(A)..3(D)
|
||||
* @param time the delay time
|
||||
* @param timeUnit the time unit
|
||||
* @param keys the key-states (true means "send")
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static String sendKeysHitDefered(int tableId, int time, LcnDefs.TimeUnit timeUnit, boolean[] keys)
|
||||
throws LcnException {
|
||||
if (tableId < 0 || tableId > 3 || keys.length != 8) {
|
||||
throw new LcnException();
|
||||
}
|
||||
StringBuilder ret = new StringBuilder("TV");
|
||||
switch (tableId) {
|
||||
case 0:
|
||||
ret.append("A");
|
||||
break;
|
||||
case 1:
|
||||
ret.append("B");
|
||||
break;
|
||||
case 2:
|
||||
ret.append("C");
|
||||
break;
|
||||
case 3:
|
||||
ret.append("D");
|
||||
break;
|
||||
default:
|
||||
throw new LcnException();
|
||||
}
|
||||
ret.append(String.format("%03d", time));
|
||||
switch (timeUnit) {
|
||||
case SECONDS:
|
||||
if (time < 1 || time > 60) {
|
||||
throw new LcnException();
|
||||
}
|
||||
ret.append("S");
|
||||
break;
|
||||
case MINUTES:
|
||||
if (time < 1 || time > 90) {
|
||||
throw new LcnException();
|
||||
}
|
||||
ret.append("M");
|
||||
break;
|
||||
case HOURS:
|
||||
if (time < 1 || time > 50) {
|
||||
throw new LcnException();
|
||||
}
|
||||
ret.append("H");
|
||||
break;
|
||||
case DAYS:
|
||||
if (time < 1 || time > 45) {
|
||||
throw new LcnException();
|
||||
}
|
||||
ret.append("D");
|
||||
break;
|
||||
default:
|
||||
throw new LcnException();
|
||||
}
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
ret.append(keys[i] ? "1" : "0");
|
||||
}
|
||||
return ret.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a request for key-lock states.
|
||||
* Always requests table A-D. Supported since LCN-PCHK 2.8.
|
||||
*
|
||||
* @return the PCK command (without address header) as text
|
||||
*/
|
||||
public static String requestKeyLocksStatus() {
|
||||
return "STX";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command to lock keys.
|
||||
*
|
||||
* @param tableId 0(A)..3(D)
|
||||
* @param states the 8 key-lock modifiers
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static String lockKeys(int tableId, LcnDefs.KeyLockStateModifier[] states) throws LcnException {
|
||||
if (tableId < 0 || tableId > 3 || states.length != 8) {
|
||||
throw new LcnException();
|
||||
}
|
||||
StringBuilder ret = new StringBuilder(
|
||||
String.format("TX%s", tableId == 0 ? "A" : tableId == 1 ? "B" : tableId == 2 ? "C" : "D"));
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
switch (states[i]) {
|
||||
case ON:
|
||||
ret.append("1");
|
||||
break;
|
||||
case OFF:
|
||||
ret.append("0");
|
||||
break;
|
||||
case TOGGLE:
|
||||
ret.append("U");
|
||||
break;
|
||||
case NOCHANGE:
|
||||
ret.append("-");
|
||||
break;
|
||||
default:
|
||||
throw new LcnException();
|
||||
}
|
||||
}
|
||||
return ret.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command to lock keys for table A temporary.
|
||||
* There is no hardware-support for locking tables B-D.
|
||||
*
|
||||
* @param time the lock time
|
||||
* @param timeUnit the time unit
|
||||
* @param keys the 8 key-lock states (true means lock)
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static String lockKeyTabATemporary(int time, LcnDefs.TimeUnit timeUnit, boolean[] keys) throws LcnException {
|
||||
if (keys.length != 8) {
|
||||
throw new LcnException();
|
||||
}
|
||||
StringBuilder ret = new StringBuilder(String.format("TXZA%03d", time));
|
||||
switch (timeUnit) {
|
||||
case SECONDS:
|
||||
if (time < 1 || time > 60) {
|
||||
throw new LcnException();
|
||||
}
|
||||
ret.append("S");
|
||||
break;
|
||||
case MINUTES:
|
||||
if (time < 1 || time > 90) {
|
||||
throw new LcnException();
|
||||
}
|
||||
ret.append("M");
|
||||
break;
|
||||
case HOURS:
|
||||
if (time < 1 || time > 50) {
|
||||
throw new LcnException();
|
||||
}
|
||||
ret.append("H");
|
||||
break;
|
||||
case DAYS:
|
||||
if (time < 1 || time > 45) {
|
||||
throw new LcnException();
|
||||
}
|
||||
ret.append("D");
|
||||
break;
|
||||
default:
|
||||
throw new LcnException();
|
||||
}
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
ret.append(keys[i] ? "1" : "0");
|
||||
}
|
||||
return ret.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the command header / start for sending dynamic texts.
|
||||
* Used by LCN-GTxD periphery (supports 4 text rows).
|
||||
* To complete the command, the text to send must be appended (UTF-8 encoding).
|
||||
* Texts are split up into up to 5 parts with 12 "UTF-8 bytes" each.
|
||||
*
|
||||
* @param row 0..3
|
||||
* @param part 0..4
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static String dynTextHeader(int row, int part) throws LcnException {
|
||||
if (row < 0 || row > 3 || part < 0 || part > 4) {
|
||||
throw new LcnException("Row number is out of range: " + (row + 1));
|
||||
}
|
||||
return String.format("GTDT%d%d", row + 1, part + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command to lock a regulator.
|
||||
*
|
||||
* @param regId 0..1
|
||||
* @param state the lock state
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static String lockRegulator(int regId, boolean state) throws LcnException {
|
||||
if (regId < 0 || regId > 1) {
|
||||
throw new LcnException();
|
||||
}
|
||||
return String.format("RE%sX%s", regId == 0 ? "A" : "B", state ? "S" : "A");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a command to start a relay timer
|
||||
*
|
||||
* @param relayNumber number of relay (1..8)
|
||||
* @param duration duration in milliseconds
|
||||
* @return the PCK command (without address header) as text
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static String startRelayTimer(int relayNumber, double duration) throws LcnException {
|
||||
if (relayNumber < 1 || relayNumber > 8 || duration < 30 || duration > 240960) {
|
||||
throw new LcnException();
|
||||
}
|
||||
StringBuilder command = new StringBuilder("R8T");
|
||||
command.append(String.format("%03d", convertMsecToLCNTimer(duration)));
|
||||
StringBuilder data = new StringBuilder("--------");
|
||||
data.setCharAt(relayNumber - 1, '1');
|
||||
command.append(data);
|
||||
return command.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a null command, used for broadcast messages.
|
||||
*
|
||||
* @return the PCK command (without address header) as text
|
||||
*/
|
||||
public static String nullCommand() {
|
||||
return "LEER";
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given time into an LCN ramp value.
|
||||
*
|
||||
* @param timeMSec the time in milliseconds
|
||||
* @return the (LCN-internal) ramp value (0..250)
|
||||
*/
|
||||
private static int timeToRampValue(int timeMSec) {
|
||||
int ret;
|
||||
if (timeMSec < 250) {
|
||||
ret = 0;
|
||||
} else if (timeMSec < 500) {
|
||||
ret = 1;
|
||||
} else if (timeMSec < 660) {
|
||||
ret = 2;
|
||||
} else if (timeMSec < 1000) {
|
||||
ret = 3;
|
||||
} else if (timeMSec < 1400) {
|
||||
ret = 4;
|
||||
} else if (timeMSec < 2000) {
|
||||
ret = 5;
|
||||
} else if (timeMSec < 3000) {
|
||||
ret = 6;
|
||||
} else if (timeMSec < 4000) {
|
||||
ret = 7;
|
||||
} else if (timeMSec < 5000) {
|
||||
ret = 8;
|
||||
} else if (timeMSec < 6000) {
|
||||
ret = 9;
|
||||
} else {
|
||||
ret = (timeMSec / 1000 - 6) / 2 + 10;
|
||||
if (ret > 250) {
|
||||
ret = 250;
|
||||
LOGGER.warn("Ramp value is too high. Limiting value to 486s.");
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts duration in milliseconds to lcntimer value
|
||||
* Source: https://www.symcon.de/forum/threads/38603-LCN-Relais-Kurzzeit-Timer-umrechnen
|
||||
*
|
||||
* @param ms time in milliseconds
|
||||
* @return lcn timer value
|
||||
*/
|
||||
private static int convertMsecToLCNTimer(double ms) {
|
||||
Integer lcntimer = -1;
|
||||
if (ms >= 0 && ms <= 240960) {
|
||||
double a = ms / 1000 / 0.03;
|
||||
double b = (a / 32.0) + 1.0;
|
||||
double c = Math.log(b) / Math.log(2);
|
||||
double mod = Math.floor(c);
|
||||
double faktor = 32 * (Math.pow(2, mod) - 1);
|
||||
double offset = (a - faktor) / Math.pow(2, mod);
|
||||
lcntimer = (int) (offset + mod * 32);
|
||||
} else {
|
||||
LOGGER.warn("Timer not in [0,240960] ms");
|
||||
}
|
||||
return lcntimer;
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.common;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Helper to bitwise reverse numbers.
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
final class ReverseNumber {
|
||||
/** Cache with all reversed 8 bit values. */
|
||||
private static final int[] REVERSED_UINT8 = new int[256];
|
||||
|
||||
/** Initializes static data once this class is first used. */
|
||||
static {
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
int reversed = 0;
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
if ((i & (1 << j)) != 0) {
|
||||
reversed |= (0x80 >> j);
|
||||
}
|
||||
}
|
||||
REVERSED_UINT8[i] = reversed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the given 8 bit value bitwise.
|
||||
*
|
||||
* @param value the value to reverse bitwise (treated as unsigned 8 bit value)
|
||||
* @return the reversed value
|
||||
* @throws LcnException if value is out of range (not unsigned 8 bit)
|
||||
*/
|
||||
static int reverseUInt8(int value) throws LcnException {
|
||||
if (value < 0 || value > 255) {
|
||||
throw new LcnException();
|
||||
}
|
||||
return REVERSED_UINT8[value];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.common;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
|
||||
/**
|
||||
* LCN variable types.
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
* @author Fabian Wolter - Migration to OH2
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum Variable {
|
||||
UNKNOWN(0, Type.UNKNOWN, LcnChannelGroup.VARIABLE), // Used if the real type is not known (yet)
|
||||
VARIABLE1(0, Type.VARIABLE, LcnChannelGroup.VARIABLE), // or TVar
|
||||
VARIABLE2(1, Type.VARIABLE, LcnChannelGroup.VARIABLE),
|
||||
VARIABLE3(2, Type.VARIABLE, LcnChannelGroup.VARIABLE),
|
||||
VARIABLE4(3, Type.VARIABLE, LcnChannelGroup.VARIABLE),
|
||||
VARIABLE5(4, Type.VARIABLE, LcnChannelGroup.VARIABLE),
|
||||
VARIABLE6(5, Type.VARIABLE, LcnChannelGroup.VARIABLE),
|
||||
VARIABLE7(6, Type.VARIABLE, LcnChannelGroup.VARIABLE),
|
||||
VARIABLE8(7, Type.VARIABLE, LcnChannelGroup.VARIABLE),
|
||||
VARIABLE9(8, Type.VARIABLE, LcnChannelGroup.VARIABLE),
|
||||
VARIABLE10(9, Type.VARIABLE, LcnChannelGroup.VARIABLE),
|
||||
VARIABLE11(10, Type.VARIABLE, LcnChannelGroup.VARIABLE),
|
||||
VARIABLE12(11, Type.VARIABLE, LcnChannelGroup.VARIABLE), // Since 170206
|
||||
RVARSETPOINT1(0, Type.REGULATOR, LcnChannelGroup.RVARSETPOINT),
|
||||
RVARSETPOINT2(1, Type.REGULATOR, LcnChannelGroup.RVARSETPOINT), // Set-points for regulators
|
||||
THRESHOLDREGISTER11(0, 0, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1),
|
||||
THRESHOLDREGISTER12(0, 1, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1),
|
||||
THRESHOLDREGISTER13(0, 2, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1),
|
||||
THRESHOLDREGISTER14(0, 3, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1),
|
||||
// Register 1 (THRESHOLDREGISTER15 only before 170206)
|
||||
THRESHOLDREGISTER15(0, 4, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1),
|
||||
THRESHOLDREGISTER21(1, 0, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER2),
|
||||
THRESHOLDREGISTER22(1, 1, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER2),
|
||||
THRESHOLDREGISTER23(1, 2, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER2),
|
||||
THRESHOLDREGISTER24(1, 3, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER2), // Register 2 (since 2012)
|
||||
THRESHOLDREGISTER31(2, 0, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER3),
|
||||
THRESHOLDREGISTER32(2, 1, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER3),
|
||||
THRESHOLDREGISTER33(2, 2, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER3),
|
||||
THRESHOLDREGISTER34(2, 3, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER3), // Register 3 (since 2012)
|
||||
THRESHOLDREGISTER41(3, 0, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER4),
|
||||
THRESHOLDREGISTER42(3, 1, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER4),
|
||||
THRESHOLDREGISTER43(3, 2, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER4),
|
||||
THRESHOLDREGISTER44(3, 3, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER4), // Register 4 (since 2012)
|
||||
S0INPUT1(0, Type.S0INPUT, LcnChannelGroup.S0INPUT),
|
||||
S0INPUT2(1, Type.S0INPUT, LcnChannelGroup.S0INPUT),
|
||||
S0INPUT3(2, Type.S0INPUT, LcnChannelGroup.S0INPUT),
|
||||
S0INPUT4(3, Type.S0INPUT, LcnChannelGroup.S0INPUT); // LCN-BU4L
|
||||
|
||||
private final int number;
|
||||
private final Optional<Integer> thresholdNumber;
|
||||
private final Type type;
|
||||
private final LcnChannelGroup channelGroup;
|
||||
|
||||
/**
|
||||
* Defines the origin of an LCN variable.
|
||||
*/
|
||||
public enum Type {
|
||||
UNKNOWN,
|
||||
VARIABLE,
|
||||
REGULATOR,
|
||||
THRESHOLD,
|
||||
S0INPUT
|
||||
}
|
||||
|
||||
Variable(int number, Type type, LcnChannelGroup channelGroup) {
|
||||
this(number, Optional.empty(), type, channelGroup);
|
||||
}
|
||||
|
||||
Variable(int number, int thresholdNumber, Type type, LcnChannelGroup channelGroup) {
|
||||
this(number, Optional.of(thresholdNumber), type, channelGroup);
|
||||
}
|
||||
|
||||
Variable(int number, Optional<Integer> thresholdNumber, Type type, LcnChannelGroup channelGroup) {
|
||||
this.number = number;
|
||||
this.type = type;
|
||||
this.channelGroup = channelGroup;
|
||||
this.thresholdNumber = thresholdNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of the variable's origin.
|
||||
*
|
||||
* @return the type
|
||||
*/
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the channel type of the variable.
|
||||
*
|
||||
* @return the channel type
|
||||
*/
|
||||
public LcnChannelGroup getChannelType() {
|
||||
return channelGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the threshold number within a threshold register.
|
||||
*
|
||||
* @return the threshold number
|
||||
*/
|
||||
public Optional<Integer> getThresholdNumber() {
|
||||
return thresholdNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the threshold register number.
|
||||
*
|
||||
* @return the threshold register number
|
||||
*/
|
||||
public int getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a given id into a variable type.
|
||||
*
|
||||
* @param number 0..11
|
||||
* @return the translated {@link Variable}
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static Variable varIdToVar(int number) throws LcnException {
|
||||
if (number < 0 || number >= LcnChannelGroup.VARIABLE.getCount()) {
|
||||
throw new LcnException("Invalid variable number: " + (number + 1));
|
||||
}
|
||||
return getVariableFromNumberAndType(number, Type.VARIABLE, v -> true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a given id into a LCN set-point variable type.
|
||||
*
|
||||
* @param number 0..1
|
||||
* @return the translated {@link Variable}
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static Variable setPointIdToVar(int number) throws LcnException {
|
||||
if (number < 0 || number >= LcnChannelGroup.RVARSETPOINT.getCount()) {
|
||||
throw new LcnException();
|
||||
}
|
||||
|
||||
return getVariableFromNumberAndType(number, Type.REGULATOR, v -> true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates given ids into a LCN threshold variable type.
|
||||
*
|
||||
* @param registerNumber 0..3
|
||||
* @param thresholdNumber 0..4 for register 0, 0..3 for registers 1..3
|
||||
* @return the translated {@link Variable}
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static Variable thrsIdToVar(int registerNumber, int thresholdNumber) throws LcnException {
|
||||
if (registerNumber < 0 || registerNumber >= LcnDefs.THRESHOLD_REGISTER_COUNT) {
|
||||
throw new LcnException("Threshold register number out of range: " + (registerNumber + 1));
|
||||
}
|
||||
if (thresholdNumber < 0 || thresholdNumber >= (registerNumber == 0 ? 5 : 4)) {
|
||||
throw new LcnException("Threshold number out of range: " + (thresholdNumber + 1));
|
||||
}
|
||||
return getVariableFromNumberAndType(registerNumber, Type.THRESHOLD,
|
||||
v -> v.thresholdNumber.get() == thresholdNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a given id into a LCN S0-input variable type.
|
||||
*
|
||||
* @param number 0..3
|
||||
* @return the translated {@link Variable}
|
||||
* @throws LcnException if out of range
|
||||
*/
|
||||
public static Variable s0IdToVar(int number) throws LcnException {
|
||||
if (number < 0 || number >= LcnChannelGroup.S0INPUT.getCount()) {
|
||||
throw new LcnException();
|
||||
}
|
||||
return getVariableFromNumberAndType(number, Type.S0INPUT, v -> true);
|
||||
}
|
||||
|
||||
private static Variable getVariableFromNumberAndType(int varId, Type type, Predicate<Variable> filter)
|
||||
throws LcnException {
|
||||
return Stream.of(values()).filter(v -> v.type == type).filter(v -> v.number == varId).filter(filter).findAny()
|
||||
.orElseThrow(LcnException::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this variable type uses special values.
|
||||
* Examples for special values: "No value yet", "sensor defective" etc.
|
||||
*
|
||||
* @return true if special values are in use
|
||||
*/
|
||||
public boolean useLcnSpecialValues() {
|
||||
return type != Type.S0INPUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module-generation check.
|
||||
* Checks if the given variable type would receive a typed response if
|
||||
* its status was requested.
|
||||
*
|
||||
* @param firmwareVersion the target LCN-modules firmware version
|
||||
* @return true if a response would contain the variable's type
|
||||
*/
|
||||
public boolean hasTypeInResponse(int firmwareVersion) {
|
||||
return (firmwareVersion >= LcnBindingConstants.FIRMWARE_2013
|
||||
|| (type != Type.VARIABLE && type != Type.REGULATOR));
|
||||
}
|
||||
|
||||
/**
|
||||
* Module-generation check.
|
||||
* Checks if the given variable type automatically sends status-updates on
|
||||
* value-change. It must be polled otherwise.
|
||||
*
|
||||
* @param firmwareVersion the target LCN-module's firmware version
|
||||
* @return true if the LCN module supports automatic status-messages for this {@link Variable}
|
||||
*/
|
||||
public boolean isEventBased(int firmwareVersion) {
|
||||
return type == Type.REGULATOR || type == Type.S0INPUT || firmwareVersion >= LcnBindingConstants.FIRMWARE_2013;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module-generation check.
|
||||
* Checks if the target LCN module would automatically send status-updates if
|
||||
* the given variable type was changed by command.
|
||||
*
|
||||
* @param variable the variable type to check
|
||||
* @param is2013 the target module's-generation
|
||||
* @return true if a poll is required to get the new status-value
|
||||
*/
|
||||
public boolean shouldPollStatusAfterCommand(int firmwareVersion) {
|
||||
// Regulator set-points will send status-messages on every change (all firmware versions)
|
||||
if (type == Type.REGULATOR) {
|
||||
return false;
|
||||
}
|
||||
// Thresholds since 170206 will send status-messages on every change
|
||||
if (firmwareVersion >= LcnBindingConstants.FIRMWARE_2013 && type == Type.THRESHOLD) {
|
||||
return false;
|
||||
}
|
||||
// Others:
|
||||
// - Variables before 170206 will never send any status-messages
|
||||
// - Variables since 170206 only send status-messages on "big" changes
|
||||
// - Thresholds before 170206 will never send any status-messages
|
||||
// - S0-inputs only send status-messages on "big" changes
|
||||
// (all "big changes" cases force us to poll the status to get faster updates)
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module-generation check.
|
||||
* Checks if the target LCN module would automatically send status-updates if
|
||||
* the given regulator's lock-state was changed by command.
|
||||
*
|
||||
* @param firmwareVersion the target LCN-module's firmware version
|
||||
* @param lockState the lock-state sent via command
|
||||
* @return true if a poll is required to get the new status-value
|
||||
*/
|
||||
public boolean shouldPollStatusAfterRegulatorLock(int firmwareVersion, boolean lockState) {
|
||||
// LCN modules before 170206 will send an automatic status-message for "lock", but not for "unlock"
|
||||
return !lockState && firmwareVersion < LcnBindingConstants.FIRMWARE_2013;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.common;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* A value of an LCN variable.
|
||||
* <p>
|
||||
* It internally stores the native LCN value and allows to convert from/into other units.
|
||||
* Some conversions allow to specify whether the source value is absolute or relative.
|
||||
* Relative values are used to create {@link VariableValue}s that can be added/subtracted from
|
||||
* other (absolute) {@link VariableValue}s.
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
* @author Fabian Wolter - Migration to OH2
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class VariableValue {
|
||||
private static final String SENSOR_DEFECTIVE_STATE = "DEFECTIVE";
|
||||
|
||||
/** The absolute, native LCN value. */
|
||||
private final long nativeValue;
|
||||
|
||||
/**
|
||||
* Constructor with native LCN value.
|
||||
*
|
||||
* @param nativeValue the native value
|
||||
*/
|
||||
public VariableValue(long nativeValue) {
|
||||
this.nativeValue = nativeValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to native value. Mask locked bit.
|
||||
*
|
||||
* @return the converted value
|
||||
*/
|
||||
public long toNative(boolean useSpecialValues) {
|
||||
if (useSpecialValues) {
|
||||
return nativeValue & 0x7fff;
|
||||
} else {
|
||||
return nativeValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lock state if value comes from a regulator set-point.
|
||||
* If the variable type is not a regulator, the result is undefined.
|
||||
*
|
||||
* @return true if the regulator is locked
|
||||
*/
|
||||
public boolean isRegulatorLocked() {
|
||||
return (this.nativeValue & 0x8000) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the defective state of the originating sensor for this variable.
|
||||
*
|
||||
* @return true if the sensor is defective
|
||||
*/
|
||||
public boolean isSensorDefective() {
|
||||
return nativeValue == 0x7f00;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration state of the variable.
|
||||
*
|
||||
* @return true if the variable is configured via LCN-PRO
|
||||
*/
|
||||
public boolean isConfigured() {
|
||||
return this.nativeValue != 0xFFFF;
|
||||
}
|
||||
|
||||
public State getState(Variable variable) {
|
||||
State stateValue;
|
||||
if (variable.useLcnSpecialValues() && isSensorDefective()) {
|
||||
stateValue = new StringType(SENSOR_DEFECTIVE_STATE);
|
||||
} else if (variable.useLcnSpecialValues() && !isConfigured()) {
|
||||
stateValue = new StringType("Not configured in LCN-PRO");
|
||||
} else {
|
||||
stateValue = new DecimalType(toNative(variable.useLcnSpecialValues()));
|
||||
}
|
||||
return stateValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.connection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.Channel;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddr;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
|
||||
/**
|
||||
* Base class representing LCN-PCK gateway connection states.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractConnectionState extends AbstractState<ConnectionStateMachine, AbstractConnectionState> {
|
||||
/** The PCK gateway's Connection */
|
||||
protected final Connection connection;
|
||||
|
||||
public AbstractConnectionState(ConnectionStateMachine context) {
|
||||
super(context);
|
||||
this.connection = context.getConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method when a PCK message has been received.
|
||||
*
|
||||
* @param data the received PCK message without line termination character
|
||||
*/
|
||||
public abstract void onPckMessageReceived(String data);
|
||||
|
||||
/**
|
||||
* Gets the framework's scheduler.
|
||||
*
|
||||
* @return the scheduler
|
||||
*/
|
||||
public ScheduledExecutorService getScheduler() {
|
||||
return context.getScheduler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a PCK message to be sent. When the connection is offline, the message will be buffered and sent when the
|
||||
* connection is established. When the enqueued PCK message is too old, it will be discarded before a new connection
|
||||
* is established.
|
||||
*
|
||||
* @param addr the module's address to which is message shall be sent
|
||||
* @param wantsAck true, if the module shall respond with an Ack upon successful processing
|
||||
* @param data the PCK message to be sent
|
||||
*/
|
||||
public void queue(LcnAddr addr, boolean wantsAck, byte[] data) {
|
||||
connection.queueOffline(addr, wantsAck, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts the Connection down finally. A shut-down connection cannot re-used.
|
||||
*/
|
||||
public void shutdownFinally() {
|
||||
nextState(ConnectionStateShutdown::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given PCK message is an LCN bus disconnect message. If so, openHAB will be informed and the
|
||||
* Connection's State Machine waits for a re-connect.
|
||||
*
|
||||
* @param pck the PCK message to check
|
||||
*/
|
||||
protected void parseLcnBusDiconnectMessage(String pck) {
|
||||
if (pck.equals(LcnDefs.LCNCONNSTATE_DISCONNECTED)) {
|
||||
connection.getCallback().onOffline("LCN bus not connected to LCN-PCHK/PKE");
|
||||
nextState(ConnectionStateWaitForLcnBusConnectedAfterDisconnected::new);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the Connection SocketChannel.
|
||||
*/
|
||||
protected void closeSocketChannel() {
|
||||
try {
|
||||
Channel socketChannel = connection.getSocketChannel();
|
||||
if (socketChannel != null) {
|
||||
socketChannel.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.connection;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
|
||||
/**
|
||||
* Base class for sending username or password.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractConnectionStateSendCredentials extends AbstractConnectionState {
|
||||
private static final int AUTH_TIMEOUT_SEC = 10;
|
||||
|
||||
public AbstractConnectionStateSendCredentials(ConnectionStateMachine context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startWorking() {
|
||||
addTimer(getScheduler().schedule(() -> nextState(ConnectionStateConnecting::new), AUTH_TIMEOUT_SEC,
|
||||
TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a timeout when the PCK gateway does not answer to the credentials.
|
||||
*/
|
||||
protected void startTimeoutTimer() {
|
||||
addTimer(getScheduler().schedule(
|
||||
() -> context.handleConnectionFailed(
|
||||
new LcnException("Network timeout in state " + getClass().getSimpleName())),
|
||||
connection.getSettings().getTimeout(), TimeUnit.MILLISECONDS));
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.connection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Base class for all states used with {@link AbstractStateMachine}.
|
||||
*
|
||||
* @param <T> type of the state machine implementation
|
||||
* @param <U> type of the state implementation
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractState<T extends AbstractStateMachine<T, U>, U extends AbstractState<T, U>> {
|
||||
private final List<ScheduledFuture<?>> usedTimers = Collections.synchronizedList(new ArrayList<>());
|
||||
protected final T context;
|
||||
|
||||
public AbstractState(T context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the State shall start its operation.
|
||||
*/
|
||||
protected abstract void startWorking();
|
||||
|
||||
/**
|
||||
* Stops all timers, the State has been started.
|
||||
*/
|
||||
protected void cancelAllTimers() {
|
||||
synchronized (usedTimers) {
|
||||
usedTimers.forEach(t -> t.cancel(true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a state starts a timer, its ScheduledFuture must be registered by this method. All timers added by this
|
||||
* method, are canceled when the StateMachine leaves this State.
|
||||
*
|
||||
* @param timer the new timer
|
||||
*/
|
||||
protected void addTimer(ScheduledFuture<?> timer) {
|
||||
usedTimers.add(timer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new State. The current state is torn down gracefully.
|
||||
*
|
||||
* @param newStateFactory the lambda returning the new State
|
||||
*/
|
||||
protected void nextState(Function<T, U> newStateFactory) {
|
||||
synchronized (context) {
|
||||
if (context.isStateActive(this)) {
|
||||
context.setState(newStateFactory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.connection;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Base class for state machines.
|
||||
*
|
||||
* @param <T> type of the state machine implementation
|
||||
* @param <U> type of the state implementation
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractStateMachine<T extends AbstractStateMachine<T, U>, U extends AbstractState<T, U>> {
|
||||
private final Logger logger = LoggerFactory.getLogger(AbstractStateMachine.class);
|
||||
/** The StateMachine's current state */
|
||||
protected @Nullable volatile U state;
|
||||
|
||||
/**
|
||||
* Sets the current state.
|
||||
*
|
||||
* @param newStateFactory the new state's factory
|
||||
*/
|
||||
protected synchronized void setState(Function<T, U> newStateFactory) {
|
||||
@Nullable
|
||||
U localState = state;
|
||||
if (localState != null) {
|
||||
localState.cancelAllTimers();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
U newState = newStateFactory.apply((T) this);
|
||||
|
||||
if (localState != null) {
|
||||
logger.debug("Changing state {} -> {}", localState.getClass().getSimpleName(),
|
||||
newState.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
state = newState;
|
||||
|
||||
state.startWorking();
|
||||
}
|
||||
|
||||
protected boolean isStateActive(AbstractState<?, ?> otherState) {
|
||||
return state == otherState; // compare by identity
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.connection;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferOverflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.AsynchronousSocketChannel;
|
||||
import java.nio.channels.Channel;
|
||||
import java.nio.channels.CompletionHandler;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddr;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddrGrp;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddrMod;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This class represents a configured connection to one LCN-PCHK.
|
||||
* It uses a {@link AsynchronousSocketChannel} to connect to LCN-PCHK.
|
||||
* Included logic:
|
||||
* <ul>
|
||||
* <li>Reconnection on connection loss
|
||||
* <li>Segment scan (to detect the local segment ID)
|
||||
* <li>Acknowledge handling
|
||||
* <li>Periodic value requests
|
||||
* <li>Caching of runtime data about the underlying LCN bus
|
||||
* </ul>
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Connection {
|
||||
private final Logger logger = LoggerFactory.getLogger(Connection.class);
|
||||
private static final int BROADCAST_MODULE_ID = 3;
|
||||
private static final int BROADCAST_SEGMENT_ID = 3;
|
||||
private final ConnectionSettings settings;
|
||||
private final ConnectionCallback callback;
|
||||
@Nullable
|
||||
private AsynchronousSocketChannel channel;
|
||||
/** The local segment id. -1 means "unknown". */
|
||||
private int localSegId;
|
||||
private final ByteBuffer readBuffer = ByteBuffer.allocate(1024);
|
||||
private final ByteArrayOutputStream sendBuffer = new ByteArrayOutputStream();
|
||||
private final Queue<@Nullable SendData> sendQueue = new LinkedBlockingQueue<>();
|
||||
private final BlockingQueue<PckQueueItem> offlineSendQueue = new LinkedBlockingQueue<>();
|
||||
private final Map<LcnAddr, ModInfo> modData = Collections.synchronizedMap(new HashMap<>());
|
||||
private volatile boolean writeInProgress;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final ConnectionStateMachine connectionStateMachine;
|
||||
|
||||
/**
|
||||
* Constructs a clean (disconnected) connection with the given settings.
|
||||
* This does not start the actual connection process.
|
||||
*
|
||||
* @param sets the settings to use for the new connection
|
||||
* @param callback the callback to the owner
|
||||
* @throws IOException
|
||||
*/
|
||||
public Connection(ConnectionSettings sets, ScheduledExecutorService scheduler, ConnectionCallback callback) {
|
||||
this.settings = sets;
|
||||
this.callback = callback;
|
||||
this.scheduler = scheduler;
|
||||
this.clearRuntimeData();
|
||||
|
||||
connectionStateMachine = new ConnectionStateMachine(this, scheduler);
|
||||
}
|
||||
|
||||
/** Clears all runtime data. */
|
||||
void clearRuntimeData() {
|
||||
this.channel = null;
|
||||
this.localSegId = -1;
|
||||
this.readBuffer.clear();
|
||||
this.sendQueue.clear();
|
||||
this.sendBuffer.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the settings for this connection (never changed).
|
||||
*
|
||||
* @return the settings
|
||||
*/
|
||||
public ConnectionSettings getSettings() {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
private boolean isSocketConnected() {
|
||||
try {
|
||||
AsynchronousSocketChannel localChannel = channel;
|
||||
return localChannel != null && localChannel.getRemoteAddress() != null;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the local segment id.
|
||||
*
|
||||
* @param localSegId the new local segment id
|
||||
*/
|
||||
public void setLocalSegId(int localSegId) {
|
||||
this.localSegId = localSegId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever an acknowledge is received.
|
||||
*
|
||||
* @param addr the source LCN module
|
||||
* @param code the LCN internal code (-1 = "positive")
|
||||
*/
|
||||
public void onAck(LcnAddrMod addr, int code) {
|
||||
synchronized (modData) {
|
||||
if (modData.containsKey(addr)) {
|
||||
modData.get(addr).onAck(code, this, this.settings.getTimeout(), System.nanoTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and/or returns cached data for the given LCN module.
|
||||
*
|
||||
* @param addr the module's address
|
||||
* @return the data
|
||||
*/
|
||||
public ModInfo updateModuleData(LcnAddrMod addr) {
|
||||
return modData.computeIfAbsent(addr, ModInfo::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and processes input from the underlying channel.
|
||||
* Fragmented input is kept in {@link #readBuffer} and will be processed with the next call.
|
||||
*
|
||||
* @throws IOException if connection was closed or a generic channel error occurred
|
||||
*/
|
||||
void readAndProcess() {
|
||||
AsynchronousSocketChannel localChannel = channel;
|
||||
if (localChannel != null && isSocketConnected()) {
|
||||
localChannel.read(readBuffer, null, new CompletionHandler<@Nullable Integer, @Nullable Void>() {
|
||||
@Override
|
||||
public void completed(@Nullable Integer transmittedByteCount, @Nullable Void attachment) {
|
||||
synchronized (Connection.this) {
|
||||
if (transmittedByteCount == null || transmittedByteCount == -1) {
|
||||
String msg = "Connection was closed by foreign host.";
|
||||
connectionStateMachine.handleConnectionFailed(new LcnException(msg));
|
||||
} else {
|
||||
// read data chunks from socket and separate frames
|
||||
readBuffer.flip();
|
||||
int aPos = readBuffer.position(); // 0
|
||||
String s = new String(readBuffer.array(), aPos, transmittedByteCount, LcnDefs.LCN_ENCODING);
|
||||
int pos1 = 0, pos2 = s.indexOf(PckGenerator.TERMINATION, pos1);
|
||||
while (pos2 != -1) {
|
||||
String data = s.substring(pos1, pos2);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Received: '{}'", data);
|
||||
}
|
||||
scheduler.submit(() -> {
|
||||
connectionStateMachine.onInputReceived(data);
|
||||
callback.onPckMessageReceived(data);
|
||||
});
|
||||
// Seek position in input array
|
||||
aPos += s.substring(pos1, pos2 + 1).getBytes(LcnDefs.LCN_ENCODING).length;
|
||||
// Next input
|
||||
pos1 = pos2 + 1;
|
||||
pos2 = s.indexOf(PckGenerator.TERMINATION, pos1);
|
||||
}
|
||||
readBuffer.limit(readBuffer.capacity());
|
||||
readBuffer.position(transmittedByteCount - aPos); // Keeps fragments for the next call
|
||||
|
||||
if (isSocketConnected()) {
|
||||
readAndProcess();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(@Nullable Throwable e, @Nullable Void attachment) {
|
||||
logger.debug("Lost connection");
|
||||
connectionStateMachine.handleConnectionFailed(e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
connectionStateMachine.handleConnectionFailed(new LcnException("Socket not open"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes all queued data.
|
||||
* Will try to write all data at once to reduce overhead.
|
||||
*/
|
||||
public synchronized void triggerWriteToSocket() {
|
||||
AsynchronousSocketChannel localChannel = channel;
|
||||
if (localChannel == null || !isSocketConnected() || writeInProgress) {
|
||||
return;
|
||||
}
|
||||
sendBuffer.reset();
|
||||
SendData item = sendQueue.poll();
|
||||
|
||||
if (item != null) {
|
||||
try {
|
||||
if (!item.write(sendBuffer, localSegId)) {
|
||||
logger.warn("Data loss: Could not write packet into send buffer");
|
||||
}
|
||||
|
||||
writeInProgress = true;
|
||||
byte[] data = sendBuffer.toByteArray();
|
||||
localChannel.write(ByteBuffer.wrap(data), null,
|
||||
new CompletionHandler<@Nullable Integer, @Nullable Void>() {
|
||||
@Override
|
||||
public void completed(@Nullable Integer result, @Nullable Void attachment) {
|
||||
synchronized (Connection.this) {
|
||||
if (result != data.length) {
|
||||
logger.warn("Data loss while writing to channel: {}", settings.getAddress());
|
||||
} else {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Sent: {}", new String(data, 0, data.length));
|
||||
}
|
||||
}
|
||||
|
||||
writeInProgress = false;
|
||||
|
||||
if (sendQueue.size() > 0) {
|
||||
/**
|
||||
* This could lead to stack overflows, since the CompletionHandler may run in
|
||||
* the same Thread as triggerWriteToSocket() is invoked (see
|
||||
* {@link AsynchronousChannelGroup}/Threading), but we do not expect as much
|
||||
* data in one chunk here, that the stack can be filled in a critical way.
|
||||
*/
|
||||
triggerWriteToSocket();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(@Nullable Throwable exc, @Nullable Void attachment) {
|
||||
synchronized (Connection.this) {
|
||||
if (exc != null) {
|
||||
logger.warn("Writing to channel \"{}\" failed: {}", settings.getAddress(),
|
||||
exc.getMessage());
|
||||
}
|
||||
writeInProgress = false;
|
||||
connectionStateMachine.handleConnectionFailed(new LcnException("write() failed"));
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (BufferOverflowException | IOException e) {
|
||||
logger.warn("Sending failed: {}: {}: {}", item, e.getClass().getSimpleName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues plain text to be sent to LCN-PCHK.
|
||||
* Sending will be done the next time {@link #triggerWriteToSocket()} is called.
|
||||
*
|
||||
* @param plainText the text
|
||||
*/
|
||||
public void queueDirectlyPlainText(String plainText) {
|
||||
this.queueAndSend(new SendDataPlainText(plainText));
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a PCK command to be sent.
|
||||
*
|
||||
* @param addr the target LCN address
|
||||
* @param wantsAck true to wait for acknowledge on receipt (should be false for group addresses)
|
||||
* @param pck the pure PCK command (without address header)
|
||||
*/
|
||||
void queueDirectly(LcnAddr addr, boolean wantsAck, String pck) {
|
||||
this.queueDirectly(addr, wantsAck, pck.getBytes(LcnDefs.LCN_ENCODING));
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a PCK command for immediate sending, regardless of the Connection state. The PCK command is automatically
|
||||
* re-sent if the destination is not a group, an Ack is requested and the module did not answer within the expected
|
||||
* time.
|
||||
*
|
||||
* @param addr the target LCN address
|
||||
* @param wantsAck true to wait for acknowledge on receipt (should be false for group addresses)
|
||||
* @param data the pure PCK command (without address header)
|
||||
*/
|
||||
void queueDirectly(LcnAddr addr, boolean wantsAck, byte[] data) {
|
||||
if (!addr.isGroup() && wantsAck) {
|
||||
this.updateModuleData((LcnAddrMod) addr).queuePckCommandWithAck(data, this, this.settings.getTimeout(),
|
||||
System.nanoTime());
|
||||
} else {
|
||||
this.queueAndSend(new SendDataPck(addr, false, data));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a raw PCK command and triggers the socket to start sending, if it does not already. Does not take care
|
||||
* of any Acks.
|
||||
*
|
||||
* @param data raw PCK command
|
||||
*/
|
||||
synchronized void queueAndSend(SendData data) {
|
||||
this.sendQueue.add(data);
|
||||
|
||||
triggerWriteToSocket();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a PCK command to the offline queue. Data will be sent when the Connection state will enter
|
||||
* {@link ConnectionStateConnected}.
|
||||
*
|
||||
* @param addr LCN module address
|
||||
* @param wantsAck true, if the LCN module shall respond with an Ack on successful processing
|
||||
* @param data the pure PCK command (without address header)
|
||||
*/
|
||||
void queueOffline(LcnAddr addr, boolean wantsAck, byte[] data) {
|
||||
offlineSendQueue.add(new PckQueueItem(addr, wantsAck, data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a PCK command for sending. Takes care of the Connection state and buffers the command for a specific
|
||||
* time if the Connection is not ready. If an Ack is requested, the PCK command is automatically
|
||||
* re-sent, if the module did not answer in the expected time.
|
||||
*
|
||||
* @param addr LCN module address
|
||||
* @param wantsAck true, if the LCN module shall respond with an Ack on successful processing
|
||||
* @param pck the pure PCK command (without address header)
|
||||
*/
|
||||
public void queue(LcnAddr addr, boolean wantsAck, String pck) {
|
||||
this.queue(addr, wantsAck, pck.getBytes(LcnDefs.LCN_ENCODING));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a PCK command for sending. Takes care of the Connection state and buffers the command for a specific
|
||||
* time if the Connection is not ready. If an Ack is requested, the PCK command is automatically
|
||||
* re-sent, if the module did not answer in the expected time.
|
||||
*
|
||||
* @param addr LCN module address
|
||||
* @param wantsAck true, if the LCN module shall respond with an Ack on successful processing
|
||||
* @param pck the pure PCK command (without address header)
|
||||
*/
|
||||
public void queue(LcnAddr addr, boolean wantsAck, byte[] pck) {
|
||||
connectionStateMachine.queue(addr, wantsAck, pck);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the offline PCK command queue. Does only send recently enqueued PCK commands, the rest is discarded.
|
||||
*/
|
||||
void sendOfflineQueue() {
|
||||
List<PckQueueItem> allItems = new ArrayList<>(offlineSendQueue.size());
|
||||
offlineSendQueue.drainTo(allItems);
|
||||
|
||||
allItems.forEach(item -> {
|
||||
// only send messages that were enqueued recently, discard older messages
|
||||
long timeout = settings.getTimeout();
|
||||
if (item.getEnqueued().isAfter(Instant.now().minus(timeout * 4, ChronoUnit.MILLIS))) {
|
||||
queueDirectly(item.getAddr(), item.isWantsAck(), item.getData());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Connection's callback.
|
||||
*
|
||||
* @return the callback
|
||||
*/
|
||||
public ConnectionCallback getCallback() {
|
||||
return callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the SocketChannel of this Connection
|
||||
*
|
||||
* @param channel the new Channel
|
||||
*/
|
||||
public void setSocketChannel(AsynchronousSocketChannel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the SocketChannel of the Connection.
|
||||
*
|
||||
* @returnthe socket channel
|
||||
*/
|
||||
@Nullable
|
||||
public Channel getSocketChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the local segment ID. When no segments are used, the local segment ID is 0.
|
||||
*
|
||||
* @return the local segment ID
|
||||
*/
|
||||
public int getLocalSegId() {
|
||||
return localSegId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the periodic updates on all ModInfos.
|
||||
*/
|
||||
public void updateModInfos() {
|
||||
synchronized (modData) {
|
||||
modData.values().forEach(i -> i.update(this, settings.getTimeout(), System.nanoTime()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an LCN module from the ModData list.
|
||||
*
|
||||
* @param addr the module's address to be removed
|
||||
*/
|
||||
public void removeLcnModule(LcnAddr addr) {
|
||||
modData.remove(addr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when this Connection shall be shut-down finally.
|
||||
*/
|
||||
public void shutdown() {
|
||||
connectionStateMachine.shutdownFinally();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a broadcast to all LCN modules with a reuqest to respond with an Ack.
|
||||
*/
|
||||
public void sendModuleDiscoveryCommand() {
|
||||
queueAndSend(new SendDataPck(new LcnAddrGrp(BROADCAST_SEGMENT_ID, BROADCAST_MODULE_ID), true,
|
||||
PckGenerator.nullCommand().getBytes(LcnDefs.LCN_ENCODING)));
|
||||
queueAndSend(new SendDataPck(new LcnAddrGrp(0, BROADCAST_MODULE_ID), true,
|
||||
PckGenerator.nullCommand().getBytes(LcnDefs.LCN_ENCODING)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the serial number and the firmware version of the given LCN module.
|
||||
*
|
||||
* @param addr module's address
|
||||
*/
|
||||
public void sendSerialNumberRequest(LcnAddrMod addr) {
|
||||
queueDirectly(addr, false, PckGenerator.requestSn());
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests theprogrammed name of the given LCN module.
|
||||
*
|
||||
* @param addr module's address
|
||||
*/
|
||||
public void sendModuleNameRequest(LcnAddrMod addr) {
|
||||
queueDirectly(addr, false, PckGenerator.requestModuleName(0));
|
||||
queueDirectly(addr, false, PckGenerator.requestModuleName(1));
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.connection;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Handles events from the connection to the LCN-PCK gateway.
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
* @author Fabian Wolter - Migration to OH2
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ConnectionCallback {
|
||||
/**
|
||||
* Invoked when the Connection to the PCK gateway is established and the LCN bus is connected to the PCK gateway.
|
||||
*/
|
||||
void onOnline();
|
||||
|
||||
/**
|
||||
* Invoked when the Connection to the PCK gateway has been closed or when the LCN bus is disconnected from the PCK
|
||||
* gateway.
|
||||
*
|
||||
* @param errorMessage the reason
|
||||
*/
|
||||
void onOffline(String errorMessage);
|
||||
|
||||
/**
|
||||
* Invoked when a PCK message has been reived from the PCK gateway.
|
||||
*
|
||||
* @param message the received message
|
||||
*/
|
||||
void onPckMessageReceived(String message);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.connection;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
|
||||
/**
|
||||
* Settings for a connection to LCN-PCHK.
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionSettings {
|
||||
|
||||
/** Unique identifier for this connection. */
|
||||
private final String id;
|
||||
|
||||
/** The user name for authentication. */
|
||||
private final String username;
|
||||
|
||||
/** The password for authentication. */
|
||||
private final String password;
|
||||
|
||||
/** The TCP/IP address or IP of the connection. */
|
||||
private final String address;
|
||||
|
||||
/** The TCP/IP port of the connection. */
|
||||
private final int port;
|
||||
|
||||
/** The dimming mode to use. */
|
||||
private final LcnDefs.OutputPortDimMode dimMode;
|
||||
|
||||
/** The status-messages mode to use. */
|
||||
private final LcnDefs.OutputPortStatusMode statusMode;
|
||||
|
||||
/** Timeout for requests. */
|
||||
private final long timeoutMSec;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param id the connnection's unique identifier
|
||||
* @param address the connection's TCP/IP address or IP
|
||||
* @param port the connection's TCP/IP port
|
||||
* @param username the user name for authentication
|
||||
* @param password the password for authentication
|
||||
* @param dimMode the dimming mode
|
||||
* @param statusMode the status-messages mode
|
||||
* @param timeout the request timeout
|
||||
*/
|
||||
public ConnectionSettings(String id, String address, int port, String username, String password,
|
||||
LcnDefs.OutputPortDimMode dimMode, LcnDefs.OutputPortStatusMode statusMode, int timeout) {
|
||||
this.id = id;
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.dimMode = dimMode;
|
||||
this.statusMode = statusMode;
|
||||
this.timeoutMSec = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the unique identifier for the connection.
|
||||
*
|
||||
* @return the unique identifier
|
||||
*/
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user name used for authentication.
|
||||
*
|
||||
* @return the user name
|
||||
*/
|
||||
public String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the password used for authentication.
|
||||
*
|
||||
* @return the password
|
||||
*/
|
||||
public String getPassword() {
|
||||
return this.password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the TCP/IP address or IP of the connection.
|
||||
*
|
||||
* @return the address or IP
|
||||
*/
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the TCP/IP port of the connection.
|
||||
*
|
||||
* @return the port
|
||||
*/
|
||||
public int getPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the dimming mode to use for the connection.
|
||||
*
|
||||
* @return the dimming mode
|
||||
*/
|
||||
public LcnDefs.OutputPortDimMode getDimMode() {
|
||||
return this.dimMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status-messages mode to use for the connection.
|
||||
*
|
||||
* @return the status-messages mode
|
||||
*/
|
||||
public LcnDefs.OutputPortStatusMode getStatusMode() {
|
||||
return this.statusMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request timeout.
|
||||
*
|
||||
* @return the timeout in milliseconds
|
||||
*/
|
||||
public long getTimeout() {
|
||||
return this.timeoutMSec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (!(o instanceof ConnectionSettings)) {
|
||||
return false;
|
||||
}
|
||||
ConnectionSettings other = (ConnectionSettings) o;
|
||||
return this.id.equals(other.id) && this.address.equals(other.address) && this.port == other.port
|
||||
&& this.username.equals(other.username) && this.password.equals(other.password)
|
||||
&& this.dimMode == other.dimMode && this.statusMode == other.statusMode
|
||||
&& this.timeoutMSec == other.timeoutMSec;
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.connection;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddr;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
|
||||
/**
|
||||
* This state is active when the connection to the LCN bus has been established successfully and data can be sent and
|
||||
* retrieved.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionStateConnected extends AbstractConnectionState {
|
||||
private static final int PING_INTERVAL_SEC = 60;
|
||||
private int pingCounter;
|
||||
|
||||
public ConnectionStateConnected(ConnectionStateMachine context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startWorking() {
|
||||
// send periodic keep-alives to keep the connection open
|
||||
addTimer(getScheduler().scheduleWithFixedDelay(
|
||||
() -> connection.queueDirectlyPlainText(PckGenerator.ping(++pingCounter)), PING_INTERVAL_SEC,
|
||||
PING_INTERVAL_SEC, TimeUnit.SECONDS));
|
||||
|
||||
// run ModInfo.update() for every LCN module
|
||||
addTimer(getScheduler().scheduleWithFixedDelay(connection::updateModInfos, 0, 1, TimeUnit.SECONDS));
|
||||
|
||||
connection.sendOfflineQueue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queue(LcnAddr addr, boolean wantsAck, byte[] data) {
|
||||
connection.queueDirectly(addr, wantsAck, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPckMessageReceived(String data) {
|
||||
parseLcnBusDiconnectMessage(data);
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.connection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.StandardSocketOptions;
|
||||
import java.nio.channels.AsynchronousSocketChannel;
|
||||
import java.nio.channels.CompletionHandler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This state is active during the socket creation, host name resolving and waiting for the TCP connection to become
|
||||
* established.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionStateConnecting extends AbstractConnectionState {
|
||||
private final Logger logger = LoggerFactory.getLogger(ConnectionStateConnecting.class);
|
||||
|
||||
public ConnectionStateConnecting(ConnectionStateMachine context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startWorking() {
|
||||
connection.clearRuntimeData();
|
||||
|
||||
logger.debug("Connecting to {}:{} ...", connection.getSettings().getAddress(),
|
||||
connection.getSettings().getPort());
|
||||
|
||||
try {
|
||||
// Open Channel by using the system-wide default AynchronousChannelGroup.
|
||||
// So, Threads are used or re-used on demand by the JVM.
|
||||
AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
|
||||
// Do not wait until some buffer is filled, send PCK commands immediately
|
||||
channel.setOption(StandardSocketOptions.TCP_NODELAY, true);
|
||||
connection.setSocketChannel(channel);
|
||||
|
||||
InetSocketAddress address = new InetSocketAddress(connection.getSettings().getAddress(),
|
||||
connection.getSettings().getPort());
|
||||
|
||||
if (address.isUnresolved()) {
|
||||
throw new LcnException("Could not resolve hostname");
|
||||
}
|
||||
|
||||
channel.connect(address, null, new CompletionHandler<@Nullable Void, @Nullable Void>() {
|
||||
@Override
|
||||
public void completed(@Nullable Void result, @Nullable Void attachment) {
|
||||
connection.readAndProcess();
|
||||
nextState(ConnectionStateSendUsername::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(@Nullable Throwable e, @Nullable Void attachment) {
|
||||
handleConnectionFailure(e);
|
||||
}
|
||||
});
|
||||
} catch (IOException | LcnException e) {
|
||||
handleConnectionFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleConnectionFailure(@Nullable Throwable e) {
|
||||
String message;
|
||||
if (e != null) {
|
||||
logger.warn("Could not connect to {}:{}: {}", connection.getSettings().getAddress(),
|
||||
connection.getSettings().getPort(), e.getMessage());
|
||||
message = e.getMessage();
|
||||
} else {
|
||||
message = "";
|
||||
}
|
||||
connection.getCallback().onOffline(message);
|
||||
context.handleConnectionFailed(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPckMessageReceived(String data) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.connection;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* This state is active when the connection failed. A grace period is enforced to prevent fast cycling through the
|
||||
* states.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionStateGracePeriodBeforeReconnect extends AbstractConnectionState {
|
||||
private static final int RECONNECT_GRACE_PERIOD_SEC = 5;
|
||||
|
||||
public ConnectionStateGracePeriodBeforeReconnect(ConnectionStateMachine context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startWorking() {
|
||||
closeSocketChannel();
|
||||
|
||||
addTimer(getScheduler().schedule(() -> nextState(ConnectionStateConnecting::new), RECONNECT_GRACE_PERIOD_SEC,
|
||||
TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPckMessageReceived(String data) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.connection;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* This is the initial state of the {@link ConnectionStateMachine}.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionStateInit extends AbstractConnectionState {
|
||||
public ConnectionStateInit(ConnectionStateMachine context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startWorking() {
|
||||
nextState(ConnectionStateConnecting::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPckMessageReceived(String data) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.connection;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddr;
|
||||
|
||||
/**
|
||||
* Implements a state machine for managing the connection to the LCN-PCK gateway. Setting states is thread-safe.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionStateMachine extends AbstractStateMachine<ConnectionStateMachine, AbstractConnectionState> {
|
||||
private final Connection connection;
|
||||
final ScheduledExecutorService scheduler;
|
||||
|
||||
public ConnectionStateMachine(Connection connection, ScheduledExecutorService scheduler) {
|
||||
this.connection = connection;
|
||||
this.scheduler = scheduler;
|
||||
|
||||
setState(ConnectionStateInit::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the framework's scheduler.
|
||||
*
|
||||
* @return the scheduler
|
||||
*/
|
||||
protected ScheduledExecutorService getScheduler() {
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the PCHK Connection object.
|
||||
*
|
||||
* @return the connection
|
||||
*/
|
||||
public Connection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a PCK command. Implementation is state dependent.
|
||||
*
|
||||
* @param addr the destination address
|
||||
* @param wantsAck true, if the module shall respond with an Ack
|
||||
* @param data the data
|
||||
*/
|
||||
public void queue(LcnAddr addr, boolean wantsAck, byte[] data) {
|
||||
AbstractConnectionState localState = state;
|
||||
if (localState != null) {
|
||||
localState.queue(addr, wantsAck, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked by any state, if the connection fails.
|
||||
*
|
||||
* @param e the cause
|
||||
*/
|
||||
public void handleConnectionFailed(@Nullable Throwable e) {
|
||||
if (!(state instanceof ConnectionStateShutdown)) {
|
||||
if (e != null) {
|
||||
connection.getCallback().onOffline(e.getMessage());
|
||||
} else {
|
||||
connection.getCallback().onOffline("");
|
||||
}
|
||||
setState(ConnectionStateGracePeriodBeforeReconnect::new);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a received PCK message by passing it to the current State.
|
||||
*
|
||||
* @param data the PCK message
|
||||
*/
|
||||
public void onInputReceived(String data) {
|
||||
AbstractConnectionState localState = state;
|
||||
if (localState != null) {
|
||||
localState.onPckMessageReceived(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts the StateMachine down finally. A shut-down StateMachine cannot be re-used.
|
||||
*/
|
||||
public void shutdownFinally() {
|
||||
AbstractConnectionState localState = state;
|
||||
if (localState != null) {
|
||||
localState.shutdownFinally();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.connection;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddrGrp;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This state discovers the LCN segment couplers.
|
||||
*
|
||||
* After the authorization against the LCN-PCK gateway was successful, the LCN segment couplers are discovery, to
|
||||
* retrieve the segment ID of the local segment. When no segment couplers were found, a timeout sets the local segment
|
||||
* ID to 0.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionStateSegmentScan extends AbstractConnectionState {
|
||||
private final Logger logger = LoggerFactory.getLogger(ConnectionStateSegmentScan.class);
|
||||
public static final Pattern PATTERN_SK_RESPONSE = Pattern
|
||||
.compile("=M(?<segId>\\d{3})(?<modId>\\d{3})\\.SK(?<id>\\d+)");
|
||||
private final RequestStatus statusSegmentScan = new RequestStatus(-1, 3, "Segment Scan");
|
||||
|
||||
public ConnectionStateSegmentScan(ConnectionStateMachine context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startWorking() {
|
||||
statusSegmentScan.refresh();
|
||||
addTimer(getScheduler().scheduleWithFixedDelay(this::update, 0, 500, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
private void update() {
|
||||
long currTime = System.nanoTime();
|
||||
try {
|
||||
if (statusSegmentScan.shouldSendNextRequest(connection.getSettings().getTimeout(), currTime)) {
|
||||
connection.queueDirectly(new LcnAddrGrp(3, 3), false, PckGenerator.segmentCouplerScan());
|
||||
statusSegmentScan.onRequestSent(currTime);
|
||||
}
|
||||
} catch (LcnException e) {
|
||||
// Give up. Probably no segments available.
|
||||
connection.setLocalSegId(0);
|
||||
logger.debug("No segment couplers detected");
|
||||
nextState(ConnectionStateConnected::new);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPckMessageReceived(String data) {
|
||||
Matcher matcher = PATTERN_SK_RESPONSE.matcher(data);
|
||||
|
||||
if (matcher.matches()) {
|
||||
// any segment coupler answered
|
||||
if (Integer.parseInt(matcher.group("segId")) == 0) {
|
||||
// local segment coupler answered
|
||||
connection.setLocalSegId(Integer.parseInt(matcher.group("id")));
|
||||
logger.debug("Local segment ID is {}", connection.getLocalSegId());
|
||||
nextState(ConnectionStateConnected::new);
|
||||
}
|
||||
}
|
||||
parseLcnBusDiconnectMessage(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.connection;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
|
||||
/**
|
||||
* Sets the dimming mode range (0-50 or 0-200) in the LCN-PCK for this connection, as configured by the user.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionStateSendDimMode extends AbstractConnectionState {
|
||||
public ConnectionStateSendDimMode(ConnectionStateMachine context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startWorking() {
|
||||
connection.queueDirectlyPlainText(PckGenerator.setOperationMode(connection.getSettings().getDimMode(),
|
||||
connection.getSettings().getStatusMode()));
|
||||
|
||||
nextState(ConnectionStateSegmentScan::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPckMessageReceived(String data) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.connection;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
|
||||
/**
|
||||
* This state sends the password during the authentication with the LCN-PCK gateway.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionStateSendPassword extends AbstractConnectionStateSendCredentials {
|
||||
public ConnectionStateSendPassword(ConnectionStateMachine context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startWorking() {
|
||||
startTimeoutTimer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPckMessageReceived(String data) {
|
||||
if (data.equals(LcnDefs.AUTH_PASSWORD)) {
|
||||
connection.queueDirectlyPlainText(connection.getSettings().getPassword());
|
||||
nextState(ConnectionStateWaitForLcnBusConnected::new);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.connection;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
|
||||
/**
|
||||
* This state sends the username during the authentication with the LCN-PCK gateway.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionStateSendUsername extends AbstractConnectionStateSendCredentials {
|
||||
public ConnectionStateSendUsername(ConnectionStateMachine context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startWorking() {
|
||||
startTimeoutTimer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPckMessageReceived(String data) {
|
||||
if (data.equals(LcnDefs.AUTH_USERNAME)) {
|
||||
connection.queueDirectlyPlainText(connection.getSettings().getUsername());
|
||||
nextState(ConnectionStateSendPassword::new);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.connection;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddr;
|
||||
|
||||
/**
|
||||
* This state is entered when the connection shall be shut-down finally. This happens when Thing.dispose() is called.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionStateShutdown extends AbstractConnectionState {
|
||||
public ConnectionStateShutdown(ConnectionStateMachine context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startWorking() {
|
||||
closeSocketChannel();
|
||||
|
||||
// end state
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queue(LcnAddr addr, boolean wantsAck, byte[] data) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPckMessageReceived(String data) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.connection;
|
||||
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
|
||||
/**
|
||||
* This state waits for the status answer of the LCN-PCK gateway after connection establishment, rather the LCN bus is
|
||||
* connected.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionStateWaitForLcnBusConnected extends AbstractConnectionState {
|
||||
private @Nullable ScheduledFuture<?> legacyTimer;
|
||||
|
||||
public ConnectionStateWaitForLcnBusConnected(ConnectionStateMachine context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startWorking() {
|
||||
// Legacy support for LCN-PCHK 2.2 and earlier:
|
||||
// There was no explicit "LCN connected" notification after successful authentication.
|
||||
// Only "LCN disconnected" would be reported immediately. That means "LCN connected" used to be the default.
|
||||
ScheduledFuture<?> localLegacyTimer = legacyTimer = getScheduler().schedule(() -> {
|
||||
connection.getCallback().onOnline();
|
||||
nextState(ConnectionStateSendDimMode::new);
|
||||
}, connection.getSettings().getTimeout(), TimeUnit.MILLISECONDS);
|
||||
addTimer(localLegacyTimer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPckMessageReceived(String data) {
|
||||
ScheduledFuture<?> localLegacyTimer = legacyTimer;
|
||||
if (data.equals(LcnDefs.LCNCONNSTATE_DISCONNECTED)) {
|
||||
if (localLegacyTimer != null) {
|
||||
localLegacyTimer.cancel(true);
|
||||
}
|
||||
connection.getCallback().onOffline("LCN bus not connected to LCN-PCHK/PKE");
|
||||
} else if (data.equals(LcnDefs.LCNCONNSTATE_CONNECTED)) {
|
||||
if (localLegacyTimer != null) {
|
||||
localLegacyTimer.cancel(true);
|
||||
}
|
||||
connection.getCallback().onOnline();
|
||||
nextState(ConnectionStateSendDimMode::new);
|
||||
} else if (data.equals(LcnDefs.INSUFFICIENT_LICENSES)) {
|
||||
context.handleConnectionFailed(
|
||||
new LcnException("LCN-PCHK/PKE has not enough licenses to handle this connection"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.connection;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* This state is entered when the LCN-PCK gateway sent a message, that the connection to the LCN bus was lost. This can
|
||||
* happen if the user plugs the USB cable to the PC coupler.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionStateWaitForLcnBusConnectedAfterDisconnected extends ConnectionStateWaitForLcnBusConnected {
|
||||
public ConnectionStateWaitForLcnBusConnectedAfterDisconnected(ConnectionStateMachine context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startWorking() {
|
||||
// nothing, don't start legacy timer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.connection;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddr;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
import org.openhab.binding.lcn.internal.common.Variable;
|
||||
import org.openhab.binding.lcn.internal.common.VariableValue;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Holds data of an LCN module.
|
||||
* <ul>
|
||||
* <li>Stores the module's firmware version (if requested)
|
||||
* <li>Manages the scheduling of status-requests
|
||||
* <li>Manages the scheduling of acknowledged commands
|
||||
* </ul>
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
* @author Fabian Wolter - Migration to OH2
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ModInfo {
|
||||
private final Logger logger = LoggerFactory.getLogger(ModInfo.class);
|
||||
/** Total number of request to sent before going into failed-state. */
|
||||
private static final int NUM_TRIES = 3;
|
||||
|
||||
/** Poll interval for status values that automatically send their values on change. */
|
||||
private static final int MAX_STATUS_EVENTBASED_VALUEAGE_MSEC = 600000;
|
||||
|
||||
/** Poll interval for status values that do not send their values on change (always polled). */
|
||||
private static final int MAX_STATUS_POLLED_VALUEAGE_MSEC = 30000;
|
||||
|
||||
/** Status request delay after a command has been send which potentially changed that status. */
|
||||
private static final int STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC = 2000;
|
||||
|
||||
/** The LCN module's address. */
|
||||
private final LcnAddr addr;
|
||||
|
||||
/** Firmware date of the LCN module. -1 means "unknown". */
|
||||
private int firmwareVersion = -1;
|
||||
|
||||
/** Firmware version request status. */
|
||||
private final RequestStatus requestFirmwareVersion = new RequestStatus(-1, NUM_TRIES, "Firmware Version");
|
||||
|
||||
/** Output-port request status (0..3). */
|
||||
private final RequestStatus[] requestStatusOutputs = new RequestStatus[LcnChannelGroup.OUTPUT.getCount()];
|
||||
|
||||
/** Relays request status (all 8). */
|
||||
private final RequestStatus requestStatusRelays = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC, NUM_TRIES,
|
||||
"Relays");
|
||||
|
||||
/** Binary-sensors request status (all 8). */
|
||||
private final RequestStatus requestStatusBinSensors = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC,
|
||||
NUM_TRIES, "Binary Sensors");
|
||||
|
||||
/**
|
||||
* Variables request status.
|
||||
* Lazy initialization: Will be filled once the firmware version is known.
|
||||
*/
|
||||
private final Map<Variable, @Nullable RequestStatus> requestStatusVars = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Caches the values of the variables, needed for changing the values.
|
||||
*/
|
||||
private final Map<Variable, VariableValue> variableValue = new HashMap<>();
|
||||
|
||||
/** LEDs and logic-operations request status (all 12+4). */
|
||||
private final RequestStatus requestStatusLedsAndLogicOps = new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC,
|
||||
NUM_TRIES, "LEDs and Logic");
|
||||
|
||||
/** Key lock-states request status (all tables, A-D). */
|
||||
private final RequestStatus requestStatusLockedKeys = new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC, NUM_TRIES,
|
||||
"Key Locks");
|
||||
|
||||
/**
|
||||
* Holds the last LCN variable requested whose response will not contain the variable's type.
|
||||
* {@link Variable#UNKNOWN} means there is currently no such request.
|
||||
*/
|
||||
private Variable lastRequestedVarWithoutTypeInResponse = Variable.UNKNOWN;
|
||||
|
||||
/**
|
||||
* List of queued PCK commands to be acknowledged by the LCN module.
|
||||
* Commands are always without address header.
|
||||
* Note that the first one might currently be "in progress".
|
||||
*/
|
||||
private final Queue<byte @Nullable []> pckCommandsWithAck = new ConcurrentLinkedQueue<>();
|
||||
|
||||
/** Status data for the currently processed {@link PckCommandWithAck}. */
|
||||
private final RequestStatus requestCurrentPckCommandWithAck = new RequestStatus(-1, NUM_TRIES, "Commands with Ack");
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param addr the module's address
|
||||
*/
|
||||
public ModInfo(LcnAddr addr) {
|
||||
this.addr = addr;
|
||||
for (int i = 0; i < LcnChannelGroup.OUTPUT.getCount(); ++i) {
|
||||
requestStatusOutputs[i] = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC, NUM_TRIES,
|
||||
"Output " + (i + 1));
|
||||
}
|
||||
|
||||
for (Variable var : Variable.values()) {
|
||||
if (var != Variable.UNKNOWN) {
|
||||
this.requestStatusVars.put(var, new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC, NUM_TRIES,
|
||||
var.getType() + " " + (var.getNumber() + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last requested variable whose response will not contain the variables type.
|
||||
*
|
||||
* @return the "typeless" variable
|
||||
*/
|
||||
public Variable getLastRequestedVarWithoutTypeInResponse() {
|
||||
return this.lastRequestedVarWithoutTypeInResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the last requested variable whose response will not contain the variables type.
|
||||
*
|
||||
* @param var the "typeless" variable
|
||||
*/
|
||||
public void setLastRequestedVarWithoutTypeInResponse(Variable var) {
|
||||
this.lastRequestedVarWithoutTypeInResponse = var;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a PCK command to be sent.
|
||||
* It will request an acknowledge from the LCN module on receipt.
|
||||
* If there is no response within the request timeout, the command is retried.
|
||||
*
|
||||
* @param data the PCK command to send (without address header)
|
||||
* @param timeoutMSec the time to wait for a response before retrying a request
|
||||
* @param currTime the current time stamp
|
||||
*/
|
||||
public void queuePckCommandWithAck(byte[] data, Connection conn, long timeoutMSec, long currTime) {
|
||||
this.pckCommandsWithAck.add(data);
|
||||
// Try to process the new acknowledged command. Will do nothing if another one is still in progress.
|
||||
this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever an acknowledge is received from the LCN module.
|
||||
*
|
||||
* @param code the LCN internal code. -1 means "positive" acknowledge
|
||||
* @param timeoutMSec the time to wait for a response before retrying a request
|
||||
* @param currTime the current time stamp
|
||||
*/
|
||||
public void onAck(int code, Connection conn, long timeoutMSec, long currTime) {
|
||||
if (this.requestCurrentPckCommandWithAck.isActive()) { // Check if we wait for an ack.
|
||||
this.pckCommandsWithAck.poll();
|
||||
this.requestCurrentPckCommandWithAck.reset();
|
||||
// Try to process next acknowledged command
|
||||
this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the next acknowledged command from the queue.
|
||||
*
|
||||
* @param conn the {@link Connection} belonging to this {@link ModInfo}
|
||||
* @param timeoutMSec the time to wait for a response before retrying a request
|
||||
* @param currTime the current time stamp
|
||||
* @return true if a new command was sent
|
||||
* @throws LcnException when a command response timed out
|
||||
*/
|
||||
private boolean tryProcessNextCommandWithAck(Connection conn, long timeoutMSec, long currTime) {
|
||||
// Use the chance to remove a failed command first
|
||||
if (this.requestCurrentPckCommandWithAck.isFailed(timeoutMSec, currTime)) {
|
||||
byte[] failedCommand = this.pckCommandsWithAck.poll();
|
||||
this.requestCurrentPckCommandWithAck.reset();
|
||||
|
||||
if (failedCommand != null) {
|
||||
logger.warn("{}: Module did not respond to command: {}", addr,
|
||||
new String(failedCommand, LcnDefs.LCN_ENCODING));
|
||||
}
|
||||
}
|
||||
// Peek new command
|
||||
if (!this.pckCommandsWithAck.isEmpty() && !this.requestCurrentPckCommandWithAck.isActive()) {
|
||||
this.requestCurrentPckCommandWithAck.nextRequestIn(0, currTime);
|
||||
}
|
||||
byte[] command = this.pckCommandsWithAck.peek();
|
||||
if (command == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (requestCurrentPckCommandWithAck.shouldSendNextRequest(timeoutMSec, currTime)) {
|
||||
conn.queueAndSend(new SendDataPck(addr, true, command));
|
||||
this.requestCurrentPckCommandWithAck.onRequestSent(currTime);
|
||||
}
|
||||
} catch (LcnException e) {
|
||||
logger.warn("{}: Could not send command: {}: {}", addr, new String(command, LcnDefs.LCN_ENCODING),
|
||||
e.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a request to retrieve the firmware version of the LCN module, if it is not known, yet.
|
||||
*/
|
||||
public void requestFirmwareVersion() {
|
||||
if (firmwareVersion == -1) {
|
||||
requestFirmwareVersion.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check if the module has the measurement processing firmware (since Feb. 2013).
|
||||
*
|
||||
* @return if the module has at least 4 threshold registers and 12 variables
|
||||
*/
|
||||
public boolean hasExtendedMeasurementProcessing() {
|
||||
if (firmwareVersion == -1) {
|
||||
logger.warn("LCN module firmware version unknown");
|
||||
return false;
|
||||
}
|
||||
return firmwareVersion >= LcnBindingConstants.FIRMWARE_2013;
|
||||
}
|
||||
|
||||
private boolean update(Connection conn, long timeoutMSec, long currTime, RequestStatus requestStatus, String pck)
|
||||
throws LcnException {
|
||||
if (requestStatus.shouldSendNextRequest(timeoutMSec, currTime)) {
|
||||
conn.queue(this.addr, false, pck);
|
||||
requestStatus.onRequestSent(currTime);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the request logic active.
|
||||
* Must be called periodically.
|
||||
*
|
||||
* @param conn the {@link Connection} belonging to this {@link ModInfo}
|
||||
* @param timeoutMSec the time to wait for a response before retrying a request
|
||||
* @param currTime the current time stamp
|
||||
*/
|
||||
void update(Connection conn, long timeoutMSec, long currTime) {
|
||||
try {
|
||||
if (update(conn, timeoutMSec, currTime, requestFirmwareVersion, PckGenerator.requestSn())) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < LcnChannelGroup.OUTPUT.getCount(); ++i) {
|
||||
if (update(conn, timeoutMSec, currTime, requestStatusOutputs[i], PckGenerator.requestOutputStatus(i))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (update(conn, timeoutMSec, currTime, requestStatusRelays, PckGenerator.requestRelaysStatus())) {
|
||||
return;
|
||||
}
|
||||
if (update(conn, timeoutMSec, currTime, requestStatusBinSensors, PckGenerator.requestBinSensorsStatus())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Variable requests
|
||||
if (this.firmwareVersion != -1) { // Firmware version is required
|
||||
// Use the chance to remove a failed "typeless variable" request
|
||||
if (lastRequestedVarWithoutTypeInResponse != Variable.UNKNOWN) {
|
||||
RequestStatus requestStatus = requestStatusVars.get(lastRequestedVarWithoutTypeInResponse);
|
||||
if (requestStatus != null && requestStatus.isTimeout(timeoutMSec, currTime)) {
|
||||
lastRequestedVarWithoutTypeInResponse = Variable.UNKNOWN;
|
||||
}
|
||||
}
|
||||
// Variables
|
||||
for (Map.Entry<Variable, @Nullable RequestStatus> kv : this.requestStatusVars.entrySet()) {
|
||||
RequestStatus requestStatus = kv.getValue();
|
||||
if (requestStatus != null && requestStatus.shouldSendNextRequest(timeoutMSec, currTime)) {
|
||||
// Detect if we can send immediately or if we have to wait for a "typeless" request first
|
||||
boolean hasTypeInResponse = kv.getKey().hasTypeInResponse(this.firmwareVersion);
|
||||
if (hasTypeInResponse || this.lastRequestedVarWithoutTypeInResponse == Variable.UNKNOWN) {
|
||||
try {
|
||||
conn.queue(this.addr, false,
|
||||
PckGenerator.requestVarStatus(kv.getKey(), this.firmwareVersion));
|
||||
requestStatus.onRequestSent(currTime);
|
||||
if (!hasTypeInResponse) {
|
||||
this.lastRequestedVarWithoutTypeInResponse = kv.getKey();
|
||||
}
|
||||
return;
|
||||
} catch (LcnException ex) {
|
||||
requestStatus.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (update(conn, timeoutMSec, currTime, requestStatusLedsAndLogicOps,
|
||||
PckGenerator.requestLedsAndLogicOpsStatus())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (update(conn, timeoutMSec, currTime, requestStatusLockedKeys, PckGenerator.requestKeyLocksStatus())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to send next acknowledged command. Will also detect failed ones.
|
||||
this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime);
|
||||
} catch (LcnException e) {
|
||||
logger.warn("{}: Failed to receive status message: {}", addr, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the LCN module's firmware date.
|
||||
*
|
||||
* @return the date
|
||||
*/
|
||||
public int getFirmwareVersion() {
|
||||
return this.firmwareVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the LCN module's firmware date.
|
||||
*
|
||||
* @param firmwareVersion the date
|
||||
*/
|
||||
public void setFirmwareVersion(int firmwareVersion) {
|
||||
this.firmwareVersion = firmwareVersion;
|
||||
|
||||
requestFirmwareVersion.onResponseReceived();
|
||||
|
||||
// increase poll interval, if the LCN module sends status updates of a variable event-based
|
||||
requestStatusVars.entrySet().stream().filter(e -> e.getKey().isEventBased(firmwareVersion)).forEach(e -> {
|
||||
RequestStatus value = e.getValue();
|
||||
if (value != null) {
|
||||
value.setMaxAgeMSec(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the variable value cache.
|
||||
*
|
||||
* @param variable the variable to update
|
||||
* @param value the new value
|
||||
*/
|
||||
public void updateVariableValue(Variable variable, VariableValue value) {
|
||||
variableValue.put(variable, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current value of a variable from the cache.
|
||||
*
|
||||
* @param variable the variable to retrieve the value for
|
||||
* @return the value of the variable
|
||||
* @throws LcnException when the variable is not in the cache
|
||||
*/
|
||||
public long getVariableValue(Variable variable) throws LcnException {
|
||||
return Optional.ofNullable(variableValue.get(variable)).map(v -> v.toNative(variable.useLcnSpecialValues()))
|
||||
.orElseThrow(() -> new LcnException("Current variable value unknown"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the current value of all dimmer outputs.
|
||||
*/
|
||||
public void refreshAllOutputs() {
|
||||
Arrays.stream(requestStatusOutputs).forEach(RequestStatus::refresh);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the current value of the given dimmer output.
|
||||
*
|
||||
* @param number 0..3
|
||||
*/
|
||||
public void refreshOutput(int number) {
|
||||
requestStatusOutputs[number].refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the current value of all relays.
|
||||
*/
|
||||
public void refreshRelays() {
|
||||
requestStatusRelays.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the current value of all binary sensor.
|
||||
*/
|
||||
public void refreshBinarySensors() {
|
||||
requestStatusBinSensors.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the current value of the given variable.
|
||||
*
|
||||
* @param variable the variable to request
|
||||
*/
|
||||
public void refreshVariable(Variable variable) {
|
||||
RequestStatus requestStatus = requestStatusVars.get(variable);
|
||||
if (requestStatus != null) {
|
||||
requestStatus.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the current value of all LEDs and logic operations.
|
||||
*/
|
||||
public void refreshLedsAndLogic() {
|
||||
requestStatusLedsAndLogicOps.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the current value of all LEDs and logic operations, after a LED has been changed by openHAB.
|
||||
*/
|
||||
public void refreshStatusLedsAnLogicAfterChange() {
|
||||
requestStatusLedsAndLogicOps.nextRequestIn(STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC, System.nanoTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the current locking states of all keys.
|
||||
*/
|
||||
public void refreshStatusLockedKeys() {
|
||||
requestStatusLockedKeys.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the current locking states of all keys, after a lock state has been changed by openHAB.
|
||||
*/
|
||||
public void refreshStatusStatusLockedKeysAfterChange() {
|
||||
requestStatusLockedKeys.nextRequestIn(STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC, System.nanoTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the value request logic, when a requested value has been received from the LCN module: Dimmer Output
|
||||
*
|
||||
* @param outputId 0..3
|
||||
*/
|
||||
public void onOutputResponseReceived(int outputId) {
|
||||
requestStatusOutputs[outputId].onResponseReceived();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the value request logic, when a requested value has been received from the LCN module: Relay
|
||||
*/
|
||||
public void onRelayResponseReceived() {
|
||||
requestStatusRelays.onResponseReceived();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the value request logic, when a requested value has been received from the LCN module: Binary Sensor
|
||||
*/
|
||||
public void onBinarySensorsResponseReceived() {
|
||||
requestStatusBinSensors.onResponseReceived();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the value request logic, when a requested value has been received from the LCN module: Variable
|
||||
*
|
||||
* @param variable the received variable type
|
||||
*/
|
||||
public void onVariableResponseReceived(Variable variable) {
|
||||
RequestStatus requestStatus = requestStatusVars.get(variable);
|
||||
if (requestStatus != null) {
|
||||
requestStatus.onResponseReceived();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the value request logic, when a requested value has been received from the LCN module: LEDs and logic
|
||||
*/
|
||||
public void onLedsAndLogicResponseReceived() {
|
||||
requestStatusLedsAndLogicOps.onResponseReceived();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the value request logic, when a requested value has been received from the LCN module: Keys lock state
|
||||
*/
|
||||
public void onLockedKeysResponseReceived() {
|
||||
requestStatusLockedKeys.onResponseReceived();
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.connection;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddr;
|
||||
|
||||
/**
|
||||
* Holds data of one PCK command with the target address and the date when the item has been enqueued.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PckQueueItem {
|
||||
private final Instant enqueued;
|
||||
private final LcnAddr addr;
|
||||
private final boolean wantsAck;
|
||||
private final byte[] data;
|
||||
|
||||
public PckQueueItem(LcnAddr addr, boolean wantsAck, byte[] data) {
|
||||
this.enqueued = Instant.now();
|
||||
this.addr = addr;
|
||||
this.wantsAck = wantsAck;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the time when this message has been enqueued.
|
||||
*
|
||||
* @return the Instant
|
||||
*/
|
||||
public Instant getEnqueued() {
|
||||
return enqueued;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the address of the destination LCN module.
|
||||
*
|
||||
* @return the address
|
||||
*/
|
||||
public LcnAddr getAddr() {
|
||||
return addr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an Ack is requested.
|
||||
*
|
||||
* @return true, if an Ack is requested
|
||||
*/
|
||||
public boolean isWantsAck() {
|
||||
return wantsAck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the raw PCK message to be sent.
|
||||
*
|
||||
* @return message as ByteBuffer
|
||||
*/
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.connection;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Manages timeout and retry logic for an LCN request.
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
* @author Fabian Wolter - Migration to OH2
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RequestStatus {
|
||||
private final Logger logger = LoggerFactory.getLogger(RequestStatus.class);
|
||||
/** Interval for forced updates. -1 if not used. */
|
||||
private volatile long maxAgeMSec;
|
||||
|
||||
/** Tells how often a request will be sent if no response was received. */
|
||||
private final int numTries;
|
||||
|
||||
/** true if request logic is activated. */
|
||||
private volatile boolean isActive;
|
||||
|
||||
/** The time the current request was sent out or 0. */
|
||||
private volatile long currRequestTimeStamp;
|
||||
|
||||
/** The time stamp of the next scheduled request or 0. */
|
||||
private volatile long nextRequestTimeStamp;
|
||||
|
||||
/** Number of retries left until the request is marked as failed. */
|
||||
private volatile int numRetriesLeft;
|
||||
private final String label;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param maxAgeMSec the forced-updates interval (-1 if not used)
|
||||
* @param numTries the maximum number of tries until the request is marked as failed
|
||||
*/
|
||||
RequestStatus(long maxAgeMSec, int numTries, String label) {
|
||||
this.maxAgeMSec = maxAgeMSec;
|
||||
this.numTries = numTries;
|
||||
this.label = label;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
/** Resets the runtime data to the initial states. */
|
||||
public synchronized void reset() {
|
||||
this.isActive = false;
|
||||
this.currRequestTimeStamp = 0;
|
||||
this.nextRequestTimeStamp = 0;
|
||||
this.numRetriesLeft = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the request logic is active.
|
||||
*
|
||||
* @return true if active
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return this.isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a request is waiting for a response.
|
||||
*
|
||||
* @return true if waiting for a response
|
||||
*/
|
||||
boolean isPending() {
|
||||
return this.currRequestTimeStamp != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the request is active and ran into timeout while waiting for a response.
|
||||
*
|
||||
* @param timeoutMSec the timeout in milliseconds
|
||||
* @param currTime the current time stamp
|
||||
* @return true if request timed out
|
||||
*/
|
||||
synchronized boolean isTimeout(long timeoutMSec, long currTime) {
|
||||
return this.isPending() && currTime - this.currRequestTimeStamp >= timeoutMSec * 1000000L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for failed requests (active and out of retries).
|
||||
*
|
||||
* @param timeoutMSec the timeout in milliseconds
|
||||
* @param currTime the current time stamp
|
||||
* @return true if no response was received and no retries are left
|
||||
*/
|
||||
synchronized boolean isFailed(long timeoutMSec, long currTime) {
|
||||
return this.isTimeout(timeoutMSec, currTime) && this.numRetriesLeft == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the next request.
|
||||
*
|
||||
* @param delayMSec the delay in milliseconds
|
||||
* @param currTime the current time stamp
|
||||
*/
|
||||
public synchronized void nextRequestIn(long delayMSec, long currTime) {
|
||||
this.isActive = true;
|
||||
this.nextRequestTimeStamp = currTime + delayMSec * 1000000L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a request to retrieve the current value.
|
||||
*/
|
||||
public synchronized void refresh() {
|
||||
nextRequestIn(0, System.nanoTime());
|
||||
this.numRetriesLeft = this.numTries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether sending a new request is required (should be called periodically).
|
||||
*
|
||||
* @param timeoutMSec the time to wait for a response before retrying the request
|
||||
* @param currTime the current time stamp
|
||||
* @return true to indicate a new request should be sent
|
||||
* @throws LcnException when a status request timed out
|
||||
*/
|
||||
synchronized boolean shouldSendNextRequest(long timeoutMSec, long currTime) throws LcnException {
|
||||
if (this.isActive) {
|
||||
if (this.nextRequestTimeStamp != 0 && currTime >= this.nextRequestTimeStamp) {
|
||||
return true;
|
||||
}
|
||||
// Retry of current request (after no response was received)
|
||||
if (this.isTimeout(timeoutMSec, currTime)) {
|
||||
if (this.numRetriesLeft > 0) {
|
||||
return true;
|
||||
} else if (isPending()) {
|
||||
currRequestTimeStamp = 0;
|
||||
throw new LcnException(label + ": Failed finally after " + numTries + " tries");
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called right after a new request has been sent.
|
||||
* Must be activated first.
|
||||
*
|
||||
* @param currTime the current time stamp
|
||||
*/
|
||||
public synchronized void onRequestSent(long currTime) {
|
||||
if (!this.isActive) {
|
||||
logger.warn("Tried to send a request which is not active");
|
||||
}
|
||||
// Updates retry counter
|
||||
if (this.currRequestTimeStamp == 0) {
|
||||
this.numRetriesLeft = this.numTries - 1;
|
||||
} else if (this.numRetriesLeft > 0) { // Should not happen if used correctly
|
||||
--this.numRetriesLeft;
|
||||
}
|
||||
// Mark request as pending
|
||||
this.currRequestTimeStamp = currTime;
|
||||
// Schedule next request
|
||||
if (this.maxAgeMSec != -1) {
|
||||
this.nextRequestIn(this.maxAgeMSec, currTime);
|
||||
} else {
|
||||
this.nextRequestTimeStamp = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Must be called when a response (requested or not) has been received. */
|
||||
public synchronized void onResponseReceived() {
|
||||
if (this.isActive) {
|
||||
this.currRequestTimeStamp = 0; // Mark request (if any) as successful
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timeout of this RequestStatus.
|
||||
*
|
||||
* @param maxAgeMSec the timeout in ms
|
||||
*/
|
||||
public void setMaxAgeMSec(long maxAgeMSec) {
|
||||
this.maxAgeMSec = maxAgeMSec;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.connection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Base class for a packet to be send to LCN-PCHK.
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
* @author Fabian Wolter - Migration to OH2
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class SendData {
|
||||
/**
|
||||
* Writes the packet's data into the given buffer.
|
||||
* Called right before the packet is actually sent to LCN-PCHK.
|
||||
*
|
||||
* @param buffer the target buffer
|
||||
* @param localSegId the local segment id
|
||||
* @return true if everything was set-up correctly and data was written
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
abstract boolean write(OutputStream buffer, int localSegId) throws IOException;
|
||||
}
|
||||
@@ -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.lcn.internal.connection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.BufferOverflowException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.LcnAddr;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
|
||||
/**
|
||||
* A PCK command to be send to LCN-PCHK.
|
||||
* It is already encoded as bytes to allow different text-encodings (ANSI, UTF-8).
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
* @author Fabian Wolter - Migration to OH2
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class SendDataPck extends SendData {
|
||||
/** The target LCN address. */
|
||||
private final LcnAddr addr;
|
||||
|
||||
/** true to acknowledge the command on receipt. */
|
||||
private final boolean wantsAck;
|
||||
|
||||
/** PCK command (without address header) encoded as bytes. */
|
||||
private final byte[] data;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param addr the target LCN address
|
||||
* @param wantsAck true to claim receipt
|
||||
* @param data the PCK command encoded as bytes
|
||||
*/
|
||||
SendDataPck(LcnAddr addr, boolean wantsAck, byte[] data) {
|
||||
this.addr = addr;
|
||||
this.wantsAck = wantsAck;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the PCK command.
|
||||
*
|
||||
* @return the PCK command encoded as bytes
|
||||
*/
|
||||
byte[] getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean write(OutputStream buffer, int localSegId) throws BufferOverflowException, IOException {
|
||||
buffer.write(PckGenerator.generateAddressHeader(this.addr, localSegId == -1 ? 0 : localSegId, this.wantsAck)
|
||||
.getBytes(LcnDefs.LCN_ENCODING));
|
||||
buffer.write(this.data);
|
||||
buffer.write(PckGenerator.TERMINATION.getBytes(LcnDefs.LCN_ENCODING));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Addr: " + addr + ": " + new String(data, 0, data.length, LcnDefs.LCN_ENCODING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.connection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
|
||||
/**
|
||||
* A plain text to be send to LCN-PCHK.
|
||||
*
|
||||
* @author Tobias Jüttner - Initial Contribution
|
||||
* @author Fabian Wolter - Migration to OH2
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class SendDataPlainText extends SendData {
|
||||
/** The text. */
|
||||
private final String text;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param text the text
|
||||
*/
|
||||
SendDataPlainText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text.
|
||||
*
|
||||
* @return the text
|
||||
*/
|
||||
String getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean write(OutputStream buffer, int localSegId) throws IOException {
|
||||
buffer.write((this.text + PckGenerator.TERMINATION).getBytes(LcnDefs.LCN_ENCODING));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.converter;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* Base class for all converters.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Converter {
|
||||
/**
|
||||
* Converts a state update from the Thing into a human readable representational State.
|
||||
*
|
||||
* @param state from the Thing
|
||||
* @return human readable representational State
|
||||
*/
|
||||
public State onStateUpdateFromHandler(State state) {
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a human readable value into LCN native value.
|
||||
*
|
||||
* @param humanReadable value to convert
|
||||
* @return the native LCN value
|
||||
*/
|
||||
public DecimalType onCommandFromItem(double humanReadable) {
|
||||
return new DecimalType(humanReadable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a human readable value into LCN native value.
|
||||
*
|
||||
* @param humanReadable value to convert
|
||||
* @return the native LCN value
|
||||
* @throws LcnException when the value could not be converted to the base unit
|
||||
*/
|
||||
public DecimalType onCommandFromItem(QuantityType<?> quantityType) throws LcnException {
|
||||
return onCommandFromItem(quantityType.doubleValue());
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.converter;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
|
||||
/**
|
||||
* Holds all Converter objects.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Converters {
|
||||
public static final Converter TEMPERATURE;
|
||||
public static final Converter LIGHT;
|
||||
public static final Converter CO2;
|
||||
public static final Converter CURRENT;
|
||||
public static final Converter VOLTAGE;
|
||||
public static final Converter ANGLE;
|
||||
public static final Converter WINDSPEED;
|
||||
public static final Converter IDENTITY;
|
||||
|
||||
static {
|
||||
TEMPERATURE = new ValueConverter(SIUnits.CELSIUS, n -> (n - 1000) / 10d, h -> Math.round(h * 10) + 1000);
|
||||
LIGHT = new ValueConverter(SmartHomeUnits.LUX, Converters::lightToHumanReadable, Converters::lightToNative);
|
||||
CO2 = new ValueConverter(SmartHomeUnits.PARTS_PER_MILLION, n -> (double) n, Math::round);
|
||||
CURRENT = new ValueConverter(SmartHomeUnits.AMPERE, n -> n / 100d, h -> Math.round(h * 100));
|
||||
VOLTAGE = new ValueConverter(SmartHomeUnits.VOLT, n -> n / 400d, h -> Math.round(h * 400));
|
||||
ANGLE = new ValueConverter(SmartHomeUnits.DEGREE_ANGLE, n -> (n - 1000) / 10d, Converters::angleToNative);
|
||||
WINDSPEED = new ValueConverter(SmartHomeUnits.METRE_PER_SECOND, n -> n / 10d, h -> Math.round(h * 10));
|
||||
IDENTITY = new ValueConverter(null, n -> (double) n, Math::round);
|
||||
}
|
||||
|
||||
private static long lightToNative(double value) {
|
||||
return Math.round(Math.log(value) * 100);
|
||||
}
|
||||
|
||||
private static double lightToHumanReadable(long value) {
|
||||
// Max. value hardware can deliver is 100klx. Apply hard limit, because higher native values lead to very big
|
||||
// lux values.
|
||||
if (value > lightToNative(100e3)) {
|
||||
return Double.NaN;
|
||||
}
|
||||
return Math.exp(value / 100d);
|
||||
}
|
||||
|
||||
private static long angleToNative(double h) {
|
||||
return (Math.round(h * 10) + 1000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.converter;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* Converter for all states representing antonyms.
|
||||
*
|
||||
* @author Thomas Weiler - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class InversionConverter extends Converter {
|
||||
/**
|
||||
* Converts a state into its antonym where applicable.
|
||||
*
|
||||
* @param state to be inverted
|
||||
* @return inverted state
|
||||
*/
|
||||
@Override
|
||||
public State onStateUpdateFromHandler(State state) {
|
||||
State convertedState = state;
|
||||
|
||||
if (state instanceof OpenClosedType) {
|
||||
convertedState = state.equals(OpenClosedType.OPEN) ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
|
||||
} else if (state instanceof OnOffType) {
|
||||
convertedState = state.equals(OnOffType.ON) ? OnOffType.OFF : OnOffType.ON;
|
||||
} else if (state instanceof UpDownType) {
|
||||
convertedState = state.equals(UpDownType.UP) ? UpDownType.DOWN : UpDownType.UP;
|
||||
}
|
||||
|
||||
return convertedState;
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.converter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Base class for S0 counter value converters.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class S0Converter extends ValueConverter {
|
||||
private final Logger logger = LoggerFactory.getLogger(S0Converter.class);
|
||||
protected double pulsesPerKwh;
|
||||
|
||||
public S0Converter(@Nullable Object parameter) {
|
||||
super(SmartHomeUnits.WATT, n -> 0d, h -> 0L);
|
||||
|
||||
if (parameter == null) {
|
||||
pulsesPerKwh = 1000;
|
||||
logger.debug("Pulses per kWh not set. Assuming 1000 imp./kWh.");
|
||||
} else if (parameter instanceof BigDecimal) {
|
||||
pulsesPerKwh = ((BigDecimal) parameter).doubleValue();
|
||||
} else {
|
||||
logger.warn("Could not parse 'pulses', unexpected type, should be float or integer: {}", parameter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long toNative(double value) {
|
||||
return Math.round(value * pulsesPerKwh / 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double toHumanReadable(long value) {
|
||||
return value / pulsesPerKwh * 1000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.converter;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Base class for all LCN variable value converters.
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ValueConverter extends Converter {
|
||||
private final Logger logger = LoggerFactory.getLogger(ValueConverter.class);
|
||||
private @Nullable final Unit<?> unit;
|
||||
private final Function<Long, Double> toHuman;
|
||||
private final Function<Double, Long> toNative;
|
||||
|
||||
public ValueConverter(@Nullable Unit<?> unit, Function<Long, Double> toHuman, Function<Double, Long> toNative) {
|
||||
this.unit = unit;
|
||||
this.toHuman = toHuman;
|
||||
this.toNative = toNative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given human readable value into the native LCN value.
|
||||
*
|
||||
* @param humanReadableValue the value to convert
|
||||
* @return the native value
|
||||
*/
|
||||
protected long toNative(double humanReadableValue) {
|
||||
return toNative.apply(humanReadableValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given native LCN value into a human readable value.
|
||||
*
|
||||
* @param nativeValue the value to convert
|
||||
* @return the human readable value
|
||||
*/
|
||||
protected double toHumanReadable(long nativeValue) {
|
||||
return toHuman.apply(nativeValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a human readable value into LCN native value.
|
||||
*
|
||||
* @param humanReadable value to convert
|
||||
* @return the native LCN value
|
||||
*/
|
||||
@Override
|
||||
public DecimalType onCommandFromItem(double humanReadable) {
|
||||
return new DecimalType(toNative(humanReadable));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a human readable value into LCN native value.
|
||||
*
|
||||
* @param humanReadable value to convert
|
||||
* @return the native LCN value
|
||||
* @throws LcnException when the value could not be converted to the base unit
|
||||
*/
|
||||
@Override
|
||||
public DecimalType onCommandFromItem(QuantityType<?> quantityType) throws LcnException {
|
||||
Unit<?> localUnit = unit;
|
||||
if (localUnit == null) {
|
||||
return onCommandFromItem(quantityType.doubleValue());
|
||||
}
|
||||
|
||||
QuantityType<?> quantityInBaseUnit = quantityType.toUnit(localUnit);
|
||||
|
||||
if (quantityInBaseUnit != null) {
|
||||
return onCommandFromItem(quantityInBaseUnit.doubleValue());
|
||||
} else {
|
||||
throw new LcnException(quantityType + ": Incompatible Channel unit configured: " + localUnit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a state update from the Thing into a human readable unit.
|
||||
*
|
||||
* @param state from the Thing
|
||||
* @return human readable State
|
||||
*/
|
||||
@Override
|
||||
public State onStateUpdateFromHandler(State state) {
|
||||
State result = state;
|
||||
|
||||
if (state instanceof DecimalType) {
|
||||
Unit<?> localUnit = unit;
|
||||
if (localUnit != null) {
|
||||
result = QuantityType.valueOf(toHumanReadable(((DecimalType) state).longValue()), localUnit);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Unexpected state type: {}", state.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.pchkdiscovery;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.thoughtworks.xstream.annotations.XStreamConverter;
|
||||
import com.thoughtworks.xstream.converters.extended.ToAttributedValueConverter;
|
||||
|
||||
/**
|
||||
* Used for deserializing the XML response of the LCN-PCHK discovery protocol.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@XStreamConverter(value = ToAttributedValueConverter.class, strings = { "content" })
|
||||
public class ExtService {
|
||||
private final int localPort;
|
||||
@SuppressWarnings("unused")
|
||||
private final String content = "";
|
||||
|
||||
public ExtService(int localPort) {
|
||||
this.localPort = localPort;
|
||||
}
|
||||
|
||||
public int getLocalPort() {
|
||||
return localPort;
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.pchkdiscovery;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Used for deserializing the XML response of the LCN-PCHK discovery protocol.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ExtServices {
|
||||
private final ExtService ExtService;
|
||||
|
||||
public ExtServices(ExtService extService) {
|
||||
ExtService = extService;
|
||||
}
|
||||
|
||||
public ExtService getExtService() {
|
||||
return ExtService;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.pchkdiscovery;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.MulticastSocket;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
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.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.thoughtworks.xstream.XStream;
|
||||
import com.thoughtworks.xstream.io.xml.StaxDriver;
|
||||
|
||||
/**
|
||||
* Discovers LCN-PCK gateways, such as LCN-PCHK.
|
||||
*
|
||||
* Scan approach:
|
||||
* 1. Determines all local network interfaces
|
||||
* 2. Send a multicast message on each interface to the PCHK multicast address 234.5.6.7 (not configurable by user).
|
||||
* 3. Evaluate multicast responses of PCK gateways in the network
|
||||
*
|
||||
* @author Fabian Wolter - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.lcn")
|
||||
public class LcnPchkDiscoveryService extends AbstractDiscoveryService {
|
||||
private final Logger logger = LoggerFactory.getLogger(LcnPchkDiscoveryService.class);
|
||||
private static final String HOSTNAME = "hostname";
|
||||
private static final String PORT = "port";
|
||||
private static final String MAC_ADDRESS = "macAddress";
|
||||
private static final String PCHK_DISCOVERY_MULTICAST_ADDRESS = "234.5.6.7";
|
||||
private static final int PCHK_DISCOVERY_PORT = 4220;
|
||||
private static final int INTERFACE_TIMEOUT_SEC = 2;
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
|
||||
.unmodifiableSet(Stream.of(LcnBindingConstants.THING_TYPE_PCK_GATEWAY).collect(Collectors.toSet()));
|
||||
private static final String DISCOVER_REQUEST = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><ServicesRequest xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"servicesrequest.xsd\"><Version major=\"1\" minor=\"0\" /><Requester requestId=\"1\" type=\"openHAB\" major=\"1\" minor=\"0\">openHAB</Requester><Requests><Request xsi:type=\"EnumServices\" major=\"1\" minor=\"0\" name=\"LcnPchkBus\" /></Requests></ServicesRequest>";
|
||||
|
||||
public LcnPchkDiscoveryService() throws IllegalArgumentException {
|
||||
super(SUPPORTED_THING_TYPES_UIDS, 0, false);
|
||||
}
|
||||
|
||||
private List<InetAddress> getLocalAddresses() {
|
||||
List<InetAddress> result = new LinkedList<>();
|
||||
try {
|
||||
for (NetworkInterface networkInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
|
||||
try {
|
||||
if (networkInterface.isUp() && !networkInterface.isLoopback()
|
||||
&& !networkInterface.isPointToPoint()) {
|
||||
result.addAll(Collections.list(networkInterface.getInetAddresses()));
|
||||
}
|
||||
} catch (SocketException exception) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
} catch (SocketException exception) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
try {
|
||||
InetAddress multicastAddress = InetAddress.getByName(PCHK_DISCOVERY_MULTICAST_ADDRESS);
|
||||
|
||||
getLocalAddresses().forEach(localInterfaceAddress -> {
|
||||
logger.debug("Searching on {} ...", localInterfaceAddress.getHostAddress());
|
||||
try (MulticastSocket socket = new MulticastSocket(PCHK_DISCOVERY_PORT)) {
|
||||
socket.setInterface(localInterfaceAddress);
|
||||
socket.setReuseAddress(true);
|
||||
socket.setSoTimeout(INTERFACE_TIMEOUT_SEC * 1000);
|
||||
socket.joinGroup(multicastAddress);
|
||||
|
||||
byte[] requestData = DISCOVER_REQUEST.getBytes(LcnDefs.LCN_ENCODING);
|
||||
DatagramPacket request = new DatagramPacket(requestData, requestData.length, multicastAddress,
|
||||
PCHK_DISCOVERY_PORT);
|
||||
socket.send(request);
|
||||
|
||||
do {
|
||||
byte[] rxbuf = new byte[8192];
|
||||
DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length);
|
||||
socket.receive(packet);
|
||||
|
||||
InetAddress addr = packet.getAddress();
|
||||
String response = new String(packet.getData(), LcnDefs.LCN_ENCODING);
|
||||
|
||||
if (response.contains("ServicesRequest")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ServicesResponse deserialized = xmlToServiceResponse(response);
|
||||
|
||||
String macAddress = deserialized.getServer().getMachineId().replace(":", "");
|
||||
ThingUID thingUid = new ThingUID(LcnBindingConstants.THING_TYPE_PCK_GATEWAY, macAddress);
|
||||
|
||||
Map<String, Object> properties = new HashMap<>(3);
|
||||
properties.put(HOSTNAME, addr.getHostAddress());
|
||||
properties.put(PORT, deserialized.getExtServices().getExtService().getLocalPort());
|
||||
properties.put(MAC_ADDRESS, macAddress);
|
||||
|
||||
DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid)
|
||||
.withProperties(properties).withRepresentationProperty(MAC_ADDRESS)
|
||||
.withLabel(deserialized.getServer().getContent() + " ("
|
||||
+ deserialized.getServer().getMachineName() + ")");
|
||||
|
||||
thingDiscovered(discoveryResult.build());
|
||||
} while (true); // left by SocketTimeoutException
|
||||
} catch (IOException e) {
|
||||
logger.debug("Discovery failed for {}: {}", localInterfaceAddress, e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (UnknownHostException e) {
|
||||
logger.warn("Discovery failed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
ServicesResponse xmlToServiceResponse(String response) {
|
||||
XStream xstream = new XStream(new StaxDriver());
|
||||
xstream.setClassLoader(getClass().getClassLoader());
|
||||
xstream.autodetectAnnotations(true);
|
||||
xstream.alias("ServicesResponse", ServicesResponse.class);
|
||||
xstream.alias("Server", Server.class);
|
||||
xstream.alias("Version", Server.class);
|
||||
xstream.alias("ExtServices", ExtServices.class);
|
||||
xstream.alias("ExtService", ExtService.class);
|
||||
|
||||
return (ServicesResponse) xstream.fromXML(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.pchkdiscovery;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
|
||||
import com.thoughtworks.xstream.annotations.XStreamConverter;
|
||||
import com.thoughtworks.xstream.converters.extended.ToAttributedValueConverter;
|
||||
|
||||
/**
|
||||
* Used for deserializing the XML response of the LCN-PCHK discovery protocol.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@XStreamConverter(value = ToAttributedValueConverter.class, strings = { "content" })
|
||||
public class Server {
|
||||
@XStreamAsAttribute
|
||||
private final int requestId;
|
||||
@XStreamAsAttribute
|
||||
private final String machineId;
|
||||
@XStreamAsAttribute
|
||||
private final String machineName;
|
||||
@XStreamAsAttribute
|
||||
private final String osShort;
|
||||
@XStreamAsAttribute
|
||||
private final String osLong;
|
||||
private final String content;
|
||||
|
||||
public Server(int requestId, String machineId, String machineName, String osShort, String osLong, String content) {
|
||||
this.requestId = requestId;
|
||||
this.machineId = machineId;
|
||||
this.machineName = machineName;
|
||||
this.osShort = osShort;
|
||||
this.osLong = osLong;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public int getRequestId() {
|
||||
return requestId;
|
||||
}
|
||||
|
||||
public String getMachineId() {
|
||||
return machineId;
|
||||
}
|
||||
|
||||
public String getOsShort() {
|
||||
return osShort;
|
||||
}
|
||||
|
||||
public String getOsLong() {
|
||||
return osLong;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public Object getMachineName() {
|
||||
return machineName;
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.pchkdiscovery;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Used for deserializing the XML response of the LCN-PCHK discovery protocol.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ServicesResponse {
|
||||
private final Version Version;
|
||||
private final Server Server;
|
||||
private final ExtServices ExtServices;
|
||||
@SuppressWarnings("unused")
|
||||
private final Object Services = new Object();
|
||||
|
||||
public ServicesResponse(Version version, Server server, ExtServices extServices) {
|
||||
this.Version = version;
|
||||
this.Server = server;
|
||||
this.ExtServices = extServices;
|
||||
}
|
||||
|
||||
public Server getServer() {
|
||||
return Server;
|
||||
}
|
||||
|
||||
public Version getVersion() {
|
||||
return Version;
|
||||
}
|
||||
|
||||
public ExtServices getExtServices() {
|
||||
return ExtServices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.pchkdiscovery;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
|
||||
|
||||
/**
|
||||
* Used for deserializing the XML response of the LCN-PCHK discovery protocol.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Version {
|
||||
@XStreamAsAttribute
|
||||
private final int major;
|
||||
@XStreamAsAttribute
|
||||
private final int minor;
|
||||
|
||||
public Version(int major, int minor) {
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
}
|
||||
|
||||
public int getMajor() {
|
||||
return major;
|
||||
}
|
||||
|
||||
public int getMinor() {
|
||||
return minor;
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.DimmerOutputCommand;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs.RelayStateModifier;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.Variable;
|
||||
import org.openhab.binding.lcn.internal.common.VariableValue;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StopMoveType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Base class for LCN module Thing sub handlers.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractLcnModuleSubHandler implements ILcnModuleSubHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(AbstractLcnModuleSubHandler.class);
|
||||
protected final LcnModuleHandler handler;
|
||||
protected final ModInfo info;
|
||||
|
||||
public AbstractLcnModuleSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
this.handler = handler;
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(String groupId) {
|
||||
// can be overwritten by subclasses.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException {
|
||||
unsupportedCommand(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, int number)
|
||||
throws LcnException {
|
||||
unsupportedCommand(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, String idWithoutGroup)
|
||||
throws LcnException {
|
||||
unsupportedCommand(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number)
|
||||
throws LcnException {
|
||||
unsupportedCommand(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandDimmerOutput(DimmerOutputCommand command, int number) throws LcnException {
|
||||
unsupportedCommand(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandString(StringType command, int number) throws LcnException {
|
||||
unsupportedCommand(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number, boolean invertUpDown)
|
||||
throws LcnException {
|
||||
unsupportedCommand(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandStopMove(StopMoveType command, LcnChannelGroup channelGroup, int number)
|
||||
throws LcnException {
|
||||
unsupportedCommand(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandHsb(HSBType command, String groupId) throws LcnException {
|
||||
unsupportedCommand(command);
|
||||
}
|
||||
|
||||
private void unsupportedCommand(Command command) {
|
||||
logger.warn("Unsupported command: {}: {}", getClass().getSimpleName(), command.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to parses the given PCK message. Fails silently to let another sub handler give the chance to process the
|
||||
* message.
|
||||
*
|
||||
* @param pck the message to process
|
||||
* @return true, if the message could be processed successfully
|
||||
*/
|
||||
public boolean tryParse(String pck) {
|
||||
Optional<Matcher> firstSuccessfulMatcher = getPckStatusMessagePatterns().stream().map(p -> p.matcher(pck))
|
||||
.filter(Matcher::matches).filter(m -> handler.isMyAddress(m.group("segId"), m.group("modId")))
|
||||
.findAny();
|
||||
|
||||
firstSuccessfulMatcher.ifPresent(matcher -> {
|
||||
try {
|
||||
handleStatusMessage(matcher);
|
||||
} catch (LcnException e) {
|
||||
logger.warn("Parse error: {}", e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
return firstSuccessfulMatcher.isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a RelayStateModifier array with all elements set to NOCHANGE.
|
||||
*
|
||||
* @return the created array
|
||||
*/
|
||||
protected RelayStateModifier[] createRelayStateModifierArray() {
|
||||
RelayStateModifier[] ret = new LcnDefs.RelayStateModifier[LcnChannelGroup.RELAY.getCount()];
|
||||
Arrays.fill(ret, LcnDefs.RelayStateModifier.NOCHANGE);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the state of the LCN module.
|
||||
*
|
||||
* @param type the channel type which shall be updated
|
||||
* @param number the Channel's number within the channel type, zero-based
|
||||
* @param state the new state
|
||||
*/
|
||||
protected void fireUpdate(LcnChannelGroup type, int number, State state) {
|
||||
handler.updateChannel(type, (number + 1) + "", state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires the current state of a Variable to openHAB. Resets running value request logic.
|
||||
*
|
||||
* @param matcher the pre-matched matcher
|
||||
* @param channelId the Channel's ID to update
|
||||
* @param variable the Variable to update
|
||||
* @return the new variable's value
|
||||
*/
|
||||
protected VariableValue fireUpdateAndReset(Matcher matcher, String channelId, Variable variable) {
|
||||
VariableValue value = new VariableValue(Long.parseLong(matcher.group("value" + channelId)));
|
||||
|
||||
info.updateVariableValue(variable, value);
|
||||
info.onVariableResponseReceived(variable);
|
||||
|
||||
fireUpdate(variable.getChannelType(), variable.getThresholdNumber().orElse(variable.getNumber()),
|
||||
value.getState(variable));
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.subhandler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.Variable;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Base class for LCN module Thing sub handlers processing variables.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractLcnModuleVariableSubHandler extends AbstractLcnModuleSubHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(AbstractLcnModuleVariableSubHandler.class);
|
||||
|
||||
public AbstractLcnModuleVariableSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(LcnChannelGroup channelGroup, int number) {
|
||||
requestVariable(info, channelGroup, number);
|
||||
info.requestFirmwareVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the current state of the given Channel.
|
||||
*
|
||||
* @param info the modules ModInfo cache
|
||||
* @param channelGroup the Channel group
|
||||
* @param number the Channel's number within the Channel group
|
||||
*/
|
||||
protected void requestVariable(ModInfo info, LcnChannelGroup channelGroup, int number) {
|
||||
try {
|
||||
Variable var = getVariable(channelGroup, number);
|
||||
info.refreshVariable(var);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Could not parse variable name: {}{}", channelGroup, (number + 1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Variable from the given parameters.
|
||||
*
|
||||
* @param channelGroup the Channel group the Variable is in
|
||||
* @param number the number of the Variable's Channel
|
||||
* @return the Variable
|
||||
* @throws IllegalArgumentException when the Channel group and number do not exist
|
||||
*/
|
||||
protected Variable getVariable(LcnChannelGroup channelGroup, int number) throws IllegalArgumentException {
|
||||
return Variable.valueOf(channelGroup.name() + (number + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the relative change between the current and the demanded value of a Variable.
|
||||
*
|
||||
* @param command the requested value
|
||||
* @param variable the Variable type
|
||||
* @return the difference
|
||||
* @throws LcnException when the difference is too big
|
||||
*/
|
||||
protected int getRelativeChange(DecimalType command, Variable variable) throws LcnException {
|
||||
// LCN doesn't support setting thresholds or variables with absolute values. So, calculate the relative change.
|
||||
int relativeVariableChange = (int) (command.longValue() - info.getVariableValue(variable));
|
||||
|
||||
int result;
|
||||
if (relativeVariableChange > 0) {
|
||||
result = Math.min(relativeVariableChange, getMaxAbsChange(variable));
|
||||
} else {
|
||||
result = Math.max(relativeVariableChange, -getMaxAbsChange(variable));
|
||||
}
|
||||
if (result != relativeVariableChange) {
|
||||
logger.warn("Relative change of {} too big, limiting: {}", variable, relativeVariableChange);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int getMaxAbsChange(Variable variable) {
|
||||
switch (variable) {
|
||||
case RVARSETPOINT1:
|
||||
case RVARSETPOINT2:
|
||||
case THRESHOLDREGISTER11:
|
||||
case THRESHOLDREGISTER12:
|
||||
case THRESHOLDREGISTER13:
|
||||
case THRESHOLDREGISTER14:
|
||||
case THRESHOLDREGISTER15:
|
||||
case THRESHOLDREGISTER21:
|
||||
case THRESHOLDREGISTER22:
|
||||
case THRESHOLDREGISTER23:
|
||||
case THRESHOLDREGISTER24:
|
||||
case THRESHOLDREGISTER31:
|
||||
case THRESHOLDREGISTER32:
|
||||
case THRESHOLDREGISTER33:
|
||||
case THRESHOLDREGISTER34:
|
||||
case THRESHOLDREGISTER41:
|
||||
case THRESHOLDREGISTER42:
|
||||
case THRESHOLDREGISTER43:
|
||||
case THRESHOLDREGISTER44:
|
||||
return 1000;
|
||||
case VARIABLE1:
|
||||
case VARIABLE2:
|
||||
case VARIABLE3:
|
||||
case VARIABLE4:
|
||||
case VARIABLE5:
|
||||
case VARIABLE6:
|
||||
case VARIABLE7:
|
||||
case VARIABLE8:
|
||||
case VARIABLE9:
|
||||
case VARIABLE10:
|
||||
case VARIABLE11:
|
||||
case VARIABLE12:
|
||||
return 4000;
|
||||
case UNKNOWN:
|
||||
case S0INPUT1:
|
||||
case S0INPUT2:
|
||||
case S0INPUT3:
|
||||
case S0INPUT4:
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.common.DimmerOutputCommand;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StopMoveType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
|
||||
/**
|
||||
* Interface for LCN module Thing sub handlers processing variables.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ILcnModuleSubHandler {
|
||||
/**
|
||||
* Gets the Patterns, the sub handler is capable to process.
|
||||
*
|
||||
* @return the Patterns
|
||||
*/
|
||||
Collection<Pattern> getPckStatusMessagePatterns();
|
||||
|
||||
/**
|
||||
* Processes the payload of a pre-matched PCK message.
|
||||
*
|
||||
* @param matcher the pre-matched matcher.
|
||||
* @throws LcnException when the message cannot be processed
|
||||
*/
|
||||
void handleStatusMessage(Matcher matcher) throws LcnException;
|
||||
|
||||
/**
|
||||
* Processes a refresh request from openHAB.
|
||||
*
|
||||
* @param channelGroup the Channel group that shall be refreshed
|
||||
* @param number the Channel number within the Channel group
|
||||
*/
|
||||
void handleRefresh(LcnChannelGroup channelGroup, int number);
|
||||
|
||||
/**
|
||||
* Processes a refresh request from openHAB.
|
||||
*
|
||||
* @param groupId the Channel ID that shall be refreshed
|
||||
*/
|
||||
void handleRefresh(String groupId);
|
||||
|
||||
/**
|
||||
* Handles a Command from openHAB.
|
||||
*
|
||||
* @param command the command to handle
|
||||
* @param channelGroup the addressed Channel group
|
||||
* @param number the Channel's number within the Channel group
|
||||
* @throws LcnException when the command could not processed
|
||||
*/
|
||||
void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException;
|
||||
|
||||
/**
|
||||
* Handles a Command from openHAB.
|
||||
*
|
||||
* @param command the command to handle
|
||||
* @param channelGroup the addressed Channel group
|
||||
* @param number the Channel's number within the Channel group
|
||||
* @throws LcnException when the command could not processed
|
||||
*/
|
||||
void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, int number) throws LcnException;
|
||||
|
||||
/**
|
||||
* Handles a Command from openHAB.
|
||||
*
|
||||
* @param command the command to handle
|
||||
* @param channelGroup the addressed Channel group
|
||||
* @param idWithoutGroup the Channel's name within the Channel group
|
||||
* @throws LcnException when the command could not processed
|
||||
*/
|
||||
void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, String idWithoutGroup)
|
||||
throws LcnException;
|
||||
|
||||
/**
|
||||
* Handles a Command from openHAB.
|
||||
*
|
||||
* @param command the command to handle
|
||||
* @param channelGroup the addressed Channel group
|
||||
* @param number the Channel's number within the Channel group
|
||||
* @throws LcnException when the command could not processed
|
||||
*/
|
||||
void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number) throws LcnException;
|
||||
|
||||
/**
|
||||
* Handles a Command from openHAB.
|
||||
*
|
||||
* @param command the command to handle
|
||||
* @param number the Channel's number within the Channel group
|
||||
* @throws LcnException when the command could not processed
|
||||
*/
|
||||
void handleCommandDimmerOutput(DimmerOutputCommand command, int number) throws LcnException;
|
||||
|
||||
/**
|
||||
* Handles a Command from openHAB.
|
||||
*
|
||||
* @param command the command to handle
|
||||
* @param number the Channel's number within the Channel group
|
||||
* @throws LcnException when the command could not processed
|
||||
*/
|
||||
void handleCommandString(StringType command, int number) throws LcnException;
|
||||
|
||||
/**
|
||||
* Handles a Command from openHAB.
|
||||
*
|
||||
* @param command the command to handle
|
||||
* @param channelGroup the addressed Channel group
|
||||
* @param number the Channel's number within the Channel group
|
||||
* @param invertUpDown true, if Up/Down is inverted
|
||||
* @throws LcnException when the command could not processed
|
||||
*/
|
||||
void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number, boolean invertUpDown)
|
||||
throws LcnException;
|
||||
|
||||
/**
|
||||
* Handles a Command from openHAB.
|
||||
*
|
||||
* @param command the command to handle
|
||||
* @param channelGroup the addressed Channel group
|
||||
* @param number the Channel's number within the Channel group
|
||||
* @throws LcnException when the command could not processed
|
||||
*/
|
||||
void handleCommandStopMove(StopMoveType command, LcnChannelGroup channelGroup, int number) throws LcnException;
|
||||
|
||||
/**
|
||||
* Handles a Command from openHAB.
|
||||
*
|
||||
* @param command the command to handle
|
||||
* @param groupId the Channel's name within the Channel group
|
||||
* @throws LcnException when the command could not processed
|
||||
*/
|
||||
void handleCommandHsb(HSBType command, String groupId) throws LcnException;
|
||||
}
|
||||
@@ -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.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
|
||||
/**
|
||||
* Handles State changes of binary sensors of an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleBinarySensorSubHandler extends AbstractLcnModuleSubHandler {
|
||||
private static final Pattern PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + "Bx(?<byteValue>\\d+)");
|
||||
|
||||
public LcnModuleBinarySensorSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(LcnChannelGroup channelGroup, int number) {
|
||||
info.refreshBinarySensors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) {
|
||||
info.onBinarySensorsResponseReceived();
|
||||
|
||||
boolean[] states = LcnDefs.getBooleanValue(Integer.parseInt(matcher.group("byteValue")));
|
||||
|
||||
IntStream.range(0, LcnChannelGroup.BINARYSENSOR.getCount())
|
||||
.forEach(i -> fireUpdate(LcnChannelGroup.BINARYSENSOR, i,
|
||||
states[i] ? OpenClosedType.OPEN : OpenClosedType.CLOSED));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Collections.singleton(PATTERN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
|
||||
/**
|
||||
* Handles State changes of transponders and remote controls of an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleCodeSubHandler extends AbstractLcnModuleSubHandler {
|
||||
private static final Pattern TRANSPONDER_PATTERN = Pattern
|
||||
.compile(LcnBindingConstants.ADDRESS_REGEX + "\\.ZT(?<byte0>\\d{3})(?<byte1>\\d{3})(?<byte2>\\d{3})");
|
||||
private static final Pattern FINGERPRINT_PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX
|
||||
+ "\\.ZF(?<byte0>[0-9A-Fa-f]{2})(?<byte1>[0-9A-Fa-f]{2})(?<byte2>[0-9A-Fa-f]{2})");
|
||||
private static final Pattern REMOTE_CONTROL_PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX
|
||||
+ "\\.ZI(?<byte0>\\d{3})(?<byte1>\\d{3})(?<byte2>\\d{3})(?<key>\\d{3})(?<action>\\d{3})");
|
||||
|
||||
public LcnModuleCodeSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(LcnChannelGroup channelGroup, int number) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) {
|
||||
String code;
|
||||
|
||||
if (matcher.pattern() == FINGERPRINT_PATTERN) {
|
||||
code = String.format("%02X%02X%02X", Integer.parseInt(matcher.group("byte0"), 16),
|
||||
Integer.parseInt(matcher.group("byte1"), 16), Integer.parseInt(matcher.group("byte2"), 16));
|
||||
} else {
|
||||
code = String.format("%02X%02X%02X", Integer.parseInt(matcher.group("byte0")),
|
||||
Integer.parseInt(matcher.group("byte1")), Integer.parseInt(matcher.group("byte2")));
|
||||
}
|
||||
|
||||
if (matcher.pattern() == TRANSPONDER_PATTERN) {
|
||||
handler.triggerChannel(LcnChannelGroup.CODE, "transponder", code);
|
||||
} else if (matcher.pattern() == FINGERPRINT_PATTERN) {
|
||||
handler.triggerChannel(LcnChannelGroup.CODE, "fingerprint", code);
|
||||
} else if (matcher.pattern() == REMOTE_CONTROL_PATTERN) {
|
||||
int keyNumber = Integer.parseInt(matcher.group("key"));
|
||||
String keyLayer;
|
||||
|
||||
if (keyNumber > 30) {
|
||||
keyLayer = "D";
|
||||
keyNumber -= 30;
|
||||
} else if (keyNumber > 20) {
|
||||
keyLayer = "C";
|
||||
keyNumber -= 20;
|
||||
} else if (keyNumber > 10) {
|
||||
keyLayer = "B";
|
||||
keyNumber -= 10;
|
||||
} else if (keyNumber > 0) {
|
||||
keyLayer = "A";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
int action = Integer.parseInt(matcher.group("action"));
|
||||
|
||||
if (action > 10) {
|
||||
handler.triggerChannel(LcnChannelGroup.CODE, "remotecontrolbatterylow", code);
|
||||
action -= 10;
|
||||
}
|
||||
|
||||
LcnDefs.SendKeyCommand actionType;
|
||||
switch (action) {
|
||||
case 1:
|
||||
actionType = LcnDefs.SendKeyCommand.HIT;
|
||||
break;
|
||||
case 2:
|
||||
actionType = LcnDefs.SendKeyCommand.MAKE;
|
||||
break;
|
||||
case 3:
|
||||
actionType = LcnDefs.SendKeyCommand.BREAK;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
handler.triggerChannel(LcnChannelGroup.CODE, "remotecontrolkey",
|
||||
keyLayer + keyNumber + ":" + actionType.name());
|
||||
|
||||
handler.triggerChannel(LcnChannelGroup.CODE, "remotecontrolcode",
|
||||
code + ":" + keyLayer + keyNumber + ":" + actionType.name());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Arrays.asList(TRANSPONDER_PATTERN, FINGERPRINT_PATTERN, REMOTE_CONTROL_PATTERN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
|
||||
/**
|
||||
* Handles 'send key' commands sent to this PCK host.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleHostCommandSubHandler extends AbstractLcnModuleSubHandler {
|
||||
private static final Pattern SEND_KEY_PATTERN = Pattern
|
||||
.compile("\\+M(?<hostId>\\d{3})(?<segId>\\d{3})(?<modId>\\d{3})\\.STH(?<byte0>\\d{3})(?<byte1>\\d{3})");
|
||||
|
||||
public LcnModuleHostCommandSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(LcnChannelGroup channelGroup, int number) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) throws LcnException {
|
||||
int keyTableAndActionMask = Integer.parseInt(matcher.group("byte0"));
|
||||
int keyNumberMask = Integer.parseInt(matcher.group("byte1"));
|
||||
|
||||
if ((keyTableAndActionMask & (1 << 6)) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// PCHK 3.22 supports only the old 'send key' command with key tables A-C
|
||||
for (int keyTableNumber = 0; keyTableNumber < LcnDefs.KEY_TABLE_COUNT_UNTIL_0C030C0; keyTableNumber++) {
|
||||
String keyTableName = LcnDefs.KeyTable.values()[keyTableNumber].name();
|
||||
|
||||
for (int keyNumber = 0; keyNumber < LcnDefs.KEY_COUNT; keyNumber++) {
|
||||
int actionRaw = (keyTableAndActionMask >> (keyTableNumber * 2)) & 3;
|
||||
|
||||
if (actionRaw > LcnDefs.SendKeyCommand.DONTSEND.getId()
|
||||
&& actionRaw <= LcnDefs.SendKeyCommand.BREAK.getId()
|
||||
&& ((1 << keyNumber) & keyNumberMask) != 0) {
|
||||
String actionName = LcnDefs.SendKeyCommand.get(actionRaw).name();
|
||||
|
||||
handler.triggerChannel(LcnChannelGroup.HOSTCOMMAND, "sendKeys",
|
||||
keyTableName + (keyNumber + 1) + ":" + actionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Collections.singleton(SEND_KEY_PATTERN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs.KeyLockStateModifier;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Handles Commands and State changes of key table locks of an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleKeyLockTableSubHandler extends AbstractLcnModuleSubHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(LcnModuleKeyLockTableSubHandler.class);
|
||||
private static final Pattern PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX
|
||||
+ "\\.TX(?<table0>\\d{3})(?<table1>\\d{3})(?<table2>\\d{3})((?<table3>\\d{3}))?");
|
||||
|
||||
public LcnModuleKeyLockTableSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(LcnChannelGroup channelGroup, int number) {
|
||||
info.refreshStatusLockedKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(String groupId) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException {
|
||||
KeyLockStateModifier[] keyLockStateModifiers = new LcnDefs.KeyLockStateModifier[channelGroup.getCount()];
|
||||
Arrays.fill(keyLockStateModifiers, LcnDefs.KeyLockStateModifier.NOCHANGE);
|
||||
keyLockStateModifiers[number] = command == OnOffType.ON ? LcnDefs.KeyLockStateModifier.ON
|
||||
: LcnDefs.KeyLockStateModifier.OFF;
|
||||
int tableId = channelGroup.ordinal() - LcnChannelGroup.KEYLOCKTABLEA.ordinal();
|
||||
handler.sendPck(PckGenerator.lockKeys(tableId, keyLockStateModifiers));
|
||||
info.refreshStatusStatusLockedKeysAfterChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) {
|
||||
info.onLockedKeysResponseReceived();
|
||||
|
||||
IntStream.range(0, LcnDefs.KEY_TABLE_COUNT).forEach(tableId -> {
|
||||
String stateString = matcher.group(String.format("table%d", tableId));
|
||||
if (stateString != null) {
|
||||
boolean[] states = LcnDefs.getBooleanValue(Integer.parseInt(stateString));
|
||||
try {
|
||||
LcnChannelGroup channelGroup = LcnChannelGroup.fromTableId(tableId);
|
||||
for (int i = 0; i < states.length; i++) {
|
||||
fireUpdate(channelGroup, i, states[i] ? OnOffType.ON : OnOffType.OFF);
|
||||
}
|
||||
} catch (LcnException e) {
|
||||
logger.warn("Failed to set key table lock state: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Collections.singleton(PATTERN);
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
|
||||
/**
|
||||
* Handles Commands and State changes of LEDs of an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleLedSubHandler extends AbstractLcnModuleSubHandler {
|
||||
public LcnModuleLedSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(LcnChannelGroup channelGroup, int number) {
|
||||
info.refreshLedsAndLogic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException {
|
||||
handleCommandString(new StringType(command.toString()), number);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandString(StringType command, int number) throws LcnException {
|
||||
handler.sendPck(PckGenerator.controlLed(number, LcnDefs.LedStatus.valueOf(command.toString())));
|
||||
info.refreshStatusLedsAnLogicAfterChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) {
|
||||
/** Status messages are handled in {@link LcnModuleLogicSubHandler}. */
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs.LogicOpStatus;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Handles State changes of logic operations of an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleLogicSubHandler extends AbstractLcnModuleSubHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(LcnModuleLogicSubHandler.class);
|
||||
private static final Pattern PATTERN_SINGLE_LOGIC = Pattern
|
||||
.compile(LcnBindingConstants.ADDRESS_REGEX + "S(?<id>\\d{1})(?<logicOpState>\\d{3})");
|
||||
private static final Pattern PATTERN_ALL = Pattern
|
||||
.compile(LcnBindingConstants.ADDRESS_REGEX + "\\.TL(?<ledStates>[AEBF]{12})(?<logicOpStates>[NTV]{4})");
|
||||
|
||||
public LcnModuleLogicSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(LcnChannelGroup channelGroup, int number) {
|
||||
info.refreshLedsAndLogic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) {
|
||||
info.onLedsAndLogicResponseReceived();
|
||||
|
||||
if (matcher.pattern() == PATTERN_ALL) {
|
||||
IntStream.range(0, LcnChannelGroup.LED.getCount()).forEach(i -> {
|
||||
switch (matcher.group("ledStates").toUpperCase().charAt(i)) {
|
||||
case 'A':
|
||||
fireLed(i, LcnDefs.LedStatus.OFF);
|
||||
break;
|
||||
case 'E':
|
||||
fireLed(i, LcnDefs.LedStatus.ON);
|
||||
break;
|
||||
case 'B':
|
||||
fireLed(i, LcnDefs.LedStatus.BLINK);
|
||||
break;
|
||||
case 'F':
|
||||
fireLed(i, LcnDefs.LedStatus.FLICKER);
|
||||
break;
|
||||
default:
|
||||
logger.warn("Failed to parse LED state: {}", matcher.group("ledStates"));
|
||||
}
|
||||
});
|
||||
IntStream.range(0, LcnChannelGroup.LOGIC.getCount()).forEach(i -> {
|
||||
switch (matcher.group("logicOpStates").toUpperCase().charAt(i)) {
|
||||
case 'N':
|
||||
fireLogic(i, LcnDefs.LogicOpStatus.NOT);
|
||||
break;
|
||||
case 'T':
|
||||
fireLogic(i, LcnDefs.LogicOpStatus.OR);
|
||||
break;
|
||||
case 'V':
|
||||
fireLogic(i, LcnDefs.LogicOpStatus.AND);
|
||||
break;
|
||||
default:
|
||||
logger.warn("Failed to parse logic state: {}", matcher.group("logicOpStates"));
|
||||
}
|
||||
});
|
||||
} else if (matcher.pattern() == PATTERN_SINGLE_LOGIC) {
|
||||
String rawState = matcher.group("logicOpState");
|
||||
|
||||
LogicOpStatus state;
|
||||
switch (rawState) {
|
||||
case "000":
|
||||
state = LcnDefs.LogicOpStatus.NOT;
|
||||
break;
|
||||
case "025":
|
||||
state = LcnDefs.LogicOpStatus.OR;
|
||||
break;
|
||||
case "050":
|
||||
state = LcnDefs.LogicOpStatus.AND;
|
||||
break;
|
||||
default:
|
||||
logger.warn("Failed to parse logic state: {}", rawState);
|
||||
return;
|
||||
}
|
||||
fireLogic(Integer.parseInt(matcher.group("id")) - 1, state);
|
||||
}
|
||||
}
|
||||
|
||||
private void fireLed(int number, LcnDefs.LedStatus status) {
|
||||
fireUpdate(LcnChannelGroup.LED, number, new StringType(status.toString()));
|
||||
}
|
||||
|
||||
private void fireLogic(int number, LcnDefs.LogicOpStatus status) {
|
||||
fireUpdate(LcnChannelGroup.LOGIC, number, new StringType(status.toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Arrays.asList(PATTERN_ALL, PATTERN_SINGLE_LOGIC);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Handle Acks received from an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleMetaAckSubHandler extends AbstractLcnModuleSubHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(LcnModuleMetaAckSubHandler.class);
|
||||
/** The pattern for the Ack PCK message */
|
||||
public static final Pattern PATTERN_POS = Pattern.compile("-M(?<segId>\\d{3})(?<modId>\\d{3})!");
|
||||
private static final Pattern PATTERN_NEG = Pattern.compile("-M(?<segId>\\d{3})(?<modId>\\d{3})(?<code>\\d+)");
|
||||
|
||||
public LcnModuleMetaAckSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(LcnChannelGroup channelGroup, int number) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) {
|
||||
if (matcher.pattern() == PATTERN_POS) {
|
||||
handler.onAckRceived();
|
||||
} else if (matcher.pattern() == PATTERN_NEG) {
|
||||
logger.warn("{}: NACK received: {}", handler.getStatusMessageAddress(),
|
||||
codeToString(Integer.parseInt(matcher.group("code"))));
|
||||
}
|
||||
}
|
||||
|
||||
private String codeToString(int code) {
|
||||
switch (code) {
|
||||
case LcnBindingConstants.CODE_ACK:
|
||||
return "ACK";
|
||||
case 5:
|
||||
return "Unknown command";
|
||||
case 6:
|
||||
return "Invalid parameter count";
|
||||
case 7:
|
||||
return "Invalid parameter";
|
||||
case 8:
|
||||
return "Command not allowed (e.g. output locked)";
|
||||
case 9:
|
||||
return "Command not allowed by module's configuration";
|
||||
case 10:
|
||||
return "Module not capable";
|
||||
case 11:
|
||||
return "Periphery missing";
|
||||
case 12:
|
||||
return "Programming mode necessary";
|
||||
case 14:
|
||||
return "Mains fuse blown";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Arrays.asList(PATTERN_POS, PATTERN_NEG);
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
|
||||
/**
|
||||
* Handles serial number and firmware versions received from an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleMetaFirmwareSubHandler extends AbstractLcnModuleSubHandler {
|
||||
/** The pattern for the serial number and firmware PCK message */
|
||||
public static final Pattern PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX
|
||||
+ "\\.SN(?<sn>[0-9|A-F]{10})(?<manu>[0-9|A-F]{2})FW(?<firmwareVersion>[0-9|A-F]{6})HW(?<hwType>\\d+)");
|
||||
|
||||
public LcnModuleMetaFirmwareSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(LcnChannelGroup channelGroup, int number) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) {
|
||||
info.setFirmwareVersion(Integer.parseInt(matcher.group("firmwareVersion"), 16));
|
||||
handler.updateSerialNumberProperty(matcher.group("sn"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Collections.singleton(PATTERN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.DimmerOutputCommand;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Handles Commands and State changes of dimmer outputs of an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleOutputSubHandler extends AbstractLcnModuleSubHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(LcnModuleOutputSubHandler.class);
|
||||
private static final int COLOR_RAMP_MS = 1000;
|
||||
private static final String OUTPUT_COLOR = "color";
|
||||
private static final Pattern PERCENT_PATTERN;
|
||||
private static final Pattern NATIVE_PATTERN;
|
||||
private volatile HSBType currentColor = new HSBType();
|
||||
private volatile PercentType output4 = new PercentType();
|
||||
|
||||
public LcnModuleOutputSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
static {
|
||||
PERCENT_PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + "A(?<outputId>\\d)(?<percent>\\d+)");
|
||||
NATIVE_PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + "O(?<outputId>\\d)(?<value>\\d+)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Arrays.asList(NATIVE_PATTERN, PERCENT_PATTERN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(LcnChannelGroup channelGroup, int number) {
|
||||
info.refreshOutput(number);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(String groupId) {
|
||||
if (OUTPUT_COLOR.equals(groupId)) {
|
||||
info.refreshAllOutputs();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException {
|
||||
// don't use OnOffType.as() here, because it returns @Nullable
|
||||
handler.sendPck(PckGenerator.dimOutput(number, command == OnOffType.ON ? 100 : 0, 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, int number)
|
||||
throws LcnException {
|
||||
handler.sendPck(PckGenerator.dimOutput(number, command.doubleValue(), 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, String idWithoutGroup)
|
||||
throws LcnException {
|
||||
if (!OUTPUT_COLOR.equals(idWithoutGroup)) {
|
||||
throw new LcnException("Unknown group ID: " + idWithoutGroup);
|
||||
}
|
||||
updateAndSendColor(new HSBType(currentColor.getHue(), currentColor.getSaturation(), command));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandHsb(HSBType command, String groupId) throws LcnException {
|
||||
if (!OUTPUT_COLOR.equals(groupId)) {
|
||||
throw new LcnException("Unknown group ID: " + groupId);
|
||||
}
|
||||
updateAndSendColor(command);
|
||||
}
|
||||
|
||||
private synchronized void updateAndSendColor(HSBType hsbType) throws LcnException {
|
||||
currentColor = hsbType;
|
||||
handler.updateChannel(LcnChannelGroup.OUTPUT, OUTPUT_COLOR, currentColor);
|
||||
|
||||
if (info.getFirmwareVersion() >= LcnBindingConstants.FIRMWARE_2014) {
|
||||
handler.sendPck(PckGenerator.dimAllOutputs(currentColor.getRed().doubleValue(),
|
||||
currentColor.getGreen().doubleValue(), currentColor.getBlue().doubleValue(), output4.doubleValue(),
|
||||
COLOR_RAMP_MS));
|
||||
} else {
|
||||
handler.sendPck(PckGenerator.dimOutput(0, currentColor.getRed().doubleValue(), COLOR_RAMP_MS));
|
||||
handler.sendPck(PckGenerator.dimOutput(1, currentColor.getGreen().doubleValue(), COLOR_RAMP_MS));
|
||||
handler.sendPck(PckGenerator.dimOutput(2, currentColor.getBlue().doubleValue(), COLOR_RAMP_MS));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandDimmerOutput(DimmerOutputCommand command, int number) throws LcnException {
|
||||
int rampMs = command.getRampMs();
|
||||
if (command.isControlAllOutputs()) { // control all dimmer outputs
|
||||
if (rampMs == LcnDefs.FIXED_RAMP_MS) {
|
||||
// compatibility command
|
||||
handler.sendPck(PckGenerator.controlAllOutputs(command.intValue()));
|
||||
} else {
|
||||
// command since firmware 180501
|
||||
handler.sendPck(PckGenerator.dimAllOutputs(command.doubleValue(), command.doubleValue(),
|
||||
command.doubleValue(), command.doubleValue(), rampMs));
|
||||
}
|
||||
} else if (command.isControlOutputs12()) { // control dimmer outputs 1+2
|
||||
if (command.intValue() == 0 || command.intValue() == 100) {
|
||||
handler.sendPck(PckGenerator.controlOutputs12(command.intValue() > 0, rampMs >= LcnDefs.FIXED_RAMP_MS));
|
||||
} else {
|
||||
// ignore ramp when dimming
|
||||
handler.sendPck(PckGenerator.dimOutputs12(command.doubleValue()));
|
||||
}
|
||||
} else {
|
||||
handler.sendPck(PckGenerator.dimOutput(number, command.doubleValue(), rampMs));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) {
|
||||
int outputId = Integer.parseInt(matcher.group("outputId")) - 1;
|
||||
|
||||
if (!LcnChannelGroup.OUTPUT.isValidId(outputId)) {
|
||||
logger.warn("outputId out of range: {}", outputId);
|
||||
return;
|
||||
}
|
||||
double percent;
|
||||
if (matcher.pattern() == PERCENT_PATTERN) {
|
||||
percent = Integer.parseInt(matcher.group("percent"));
|
||||
} else if (matcher.pattern() == NATIVE_PATTERN) {
|
||||
percent = (double) Integer.parseInt(matcher.group("value")) / 2;
|
||||
} else {
|
||||
logger.warn("Unexpected pattern: {}", matcher.pattern());
|
||||
return;
|
||||
}
|
||||
|
||||
info.onOutputResponseReceived(outputId);
|
||||
|
||||
percent = Math.min(100, Math.max(0, percent));
|
||||
|
||||
PercentType percentType = new PercentType((int) Math.round(percent));
|
||||
fireUpdate(LcnChannelGroup.OUTPUT, outputId, percentType);
|
||||
|
||||
if (outputId == 3) {
|
||||
output4 = percentType;
|
||||
}
|
||||
|
||||
if (percent > 0) {
|
||||
if (outputId == 0) {
|
||||
fireUpdate(LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0, UpDownType.UP);
|
||||
} else if (outputId == 1) {
|
||||
fireUpdate(LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0, UpDownType.DOWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs.RelayStateModifier;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
|
||||
/**
|
||||
* Handles Commands and State changes of Relays of an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleRelaySubHandler extends AbstractLcnModuleSubHandler {
|
||||
private static final Pattern PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + "Rx(?<byteValue>\\d+)");
|
||||
|
||||
public LcnModuleRelaySubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(LcnChannelGroup channelGroup, int number) {
|
||||
info.refreshRelays();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException {
|
||||
RelayStateModifier[] relayStateModifiers = createRelayStateModifierArray();
|
||||
relayStateModifiers[number] = command == OnOffType.ON ? LcnDefs.RelayStateModifier.ON
|
||||
: LcnDefs.RelayStateModifier.OFF;
|
||||
handler.sendPck(PckGenerator.controlRelays(relayStateModifiers));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, int number)
|
||||
throws LcnException {
|
||||
// don't use OnOffType.as(), because it returns @Nullable
|
||||
handleCommandOnOff(command.intValue() > 0 ? OnOffType.ON : OnOffType.OFF, channelGroup, number);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) {
|
||||
info.onRelayResponseReceived();
|
||||
|
||||
boolean[] states = LcnDefs.getBooleanValue(Integer.parseInt(matcher.group("byteValue")));
|
||||
|
||||
IntStream.range(0, LcnChannelGroup.RELAY.getCount())
|
||||
.forEach(i -> fireUpdate(LcnChannelGroup.RELAY, i, OnOffType.from(states[i])));
|
||||
|
||||
IntStream.range(0, LcnChannelGroup.ROLLERSHUTTERRELAY.getCount()).forEach(i -> {
|
||||
UpDownType state = states[i * 2 + 1] ? UpDownType.DOWN : UpDownType.UP;
|
||||
fireUpdate(LcnChannelGroup.ROLLERSHUTTERRELAY, i, state);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Collections.singleton(PATTERN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.StopMoveType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
|
||||
/**
|
||||
* Handles Commands and State changes of roller shutters connected to dimmer outputs of an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleRollershutterOutputSubHandler extends AbstractLcnModuleSubHandler {
|
||||
public LcnModuleRollershutterOutputSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(LcnChannelGroup channelGroup, int number) {
|
||||
info.refreshOutput(number);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number, boolean invertUpDown)
|
||||
throws LcnException {
|
||||
// When configured as shutter in LCN-PRO, an output gets switched off, when the other is
|
||||
// switched on and vice versa.
|
||||
if (command == UpDownType.UP ^ invertUpDown) {
|
||||
// first output: 100%
|
||||
handler.sendPck(PckGenerator.dimOutput(0, 100, LcnDefs.ROLLER_SHUTTER_RAMP_MS));
|
||||
} else {
|
||||
// second output: 100%
|
||||
handler.sendPck(PckGenerator.dimOutput(1, 100, LcnDefs.ROLLER_SHUTTER_RAMP_MS));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandStopMove(StopMoveType command, LcnChannelGroup channelGroup, int number)
|
||||
throws LcnException {
|
||||
if (command == StopMoveType.STOP) {
|
||||
// both outputs off
|
||||
handler.sendPck(PckGenerator.dimOutput(0, 0, 0));
|
||||
handler.sendPck(PckGenerator.dimOutput(1, 0, 0));
|
||||
} else {
|
||||
// roller shutters on outputs are stateless, assume always down when MOVE is sent
|
||||
// second output: 100%
|
||||
handler.sendPck(PckGenerator.dimOutput(1, 100, LcnDefs.ROLLER_SHUTTER_RAMP_MS));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) {
|
||||
// status messages of roller shutters on dimmer outputs are handled in the dimmer output sub handler
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs.RelayStateModifier;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.StopMoveType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
|
||||
/**
|
||||
* Handles Commands and State changes of roller shutters connected to relays of an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleRollershutterRelaySubHandler extends AbstractLcnModuleSubHandler {
|
||||
public LcnModuleRollershutterRelaySubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(LcnChannelGroup channelGroup, int number) {
|
||||
info.refreshRelays();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number, boolean invertUpDown)
|
||||
throws LcnException {
|
||||
RelayStateModifier[] relayStateModifiers = createRelayStateModifierArray();
|
||||
// direction relay
|
||||
relayStateModifiers[number * 2 + 1] = command == UpDownType.DOWN ^ invertUpDown ? LcnDefs.RelayStateModifier.ON
|
||||
: LcnDefs.RelayStateModifier.OFF;
|
||||
// power relay
|
||||
relayStateModifiers[number * 2] = LcnDefs.RelayStateModifier.ON;
|
||||
handler.sendPck(PckGenerator.controlRelays(relayStateModifiers));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandStopMove(StopMoveType command, LcnChannelGroup channelGroup, int number)
|
||||
throws LcnException {
|
||||
RelayStateModifier[] relayStateModifiers = createRelayStateModifierArray();
|
||||
// power relay
|
||||
relayStateModifiers[number * 2] = command == StopMoveType.MOVE ? LcnDefs.RelayStateModifier.ON
|
||||
: LcnDefs.RelayStateModifier.OFF;
|
||||
handler.sendPck(PckGenerator.controlRelays(relayStateModifiers));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) {
|
||||
// status messages of roller shutters on relays are handled in the relay sub handler
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
import org.openhab.binding.lcn.internal.common.Variable;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
|
||||
/**
|
||||
* Handles Commands and State changes of regulator locks of an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleRvarLockSubHandler extends AbstractLcnModuleVariableSubHandler {
|
||||
public LcnModuleRvarLockSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRefresh(LcnChannelGroup channelGroup, int number) {
|
||||
super.handleRefresh(LcnChannelGroup.RVARSETPOINT, number);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException {
|
||||
boolean locked = command == OnOffType.ON;
|
||||
handler.sendPck(PckGenerator.lockRegulator(number, locked));
|
||||
|
||||
// request new lock state, if the module doesn't send it on itself
|
||||
Variable variable = getVariable(LcnChannelGroup.RVARSETPOINT, number);
|
||||
if (variable.shouldPollStatusAfterRegulatorLock(info.getFirmwareVersion(), locked)) {
|
||||
info.refreshVariable(variable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) {
|
||||
// status messages are handled in the RVar setpoint sub handler
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
import org.openhab.binding.lcn.internal.common.Variable;
|
||||
import org.openhab.binding.lcn.internal.common.VariableValue;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
|
||||
/**
|
||||
* Handles Commands and State changes of regulator setpoints of an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleRvarSetpointSubHandler extends AbstractLcnModuleVariableSubHandler {
|
||||
private static final Pattern PATTERN = Pattern
|
||||
.compile(LcnBindingConstants.ADDRESS_REGEX + "\\.S(?<id>\\d)(?<value>\\d+)");
|
||||
|
||||
public LcnModuleRvarSetpointSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number)
|
||||
throws LcnException {
|
||||
Variable variable = getVariable(channelGroup, number);
|
||||
|
||||
if (info.hasExtendedMeasurementProcessing()) {
|
||||
handler.sendPck(PckGenerator.setSetpointAbsolute(number, command.intValue()));
|
||||
} else {
|
||||
try {
|
||||
int relativeVariableChange = getRelativeChange(command, variable);
|
||||
handler.sendPck(
|
||||
PckGenerator.setSetpointRelative(number, LcnDefs.RelVarRef.CURRENT, relativeVariableChange));
|
||||
} catch (LcnException e) {
|
||||
// current value unknown for some reason, refresh it in case we come again here
|
||||
info.refreshVariable(variable);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) throws LcnException {
|
||||
Variable variable = Variable.setPointIdToVar(Integer.parseInt(matcher.group("id")) - 1);
|
||||
VariableValue value = fireUpdateAndReset(matcher, "", variable);
|
||||
|
||||
fireUpdate(LcnChannelGroup.RVARLOCK, variable.getNumber(), OnOffType.from(value.isRegulatorLocked()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Collections.singleton(PATTERN);
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.Variable;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
|
||||
/**
|
||||
* Handles Commands and State changes of S0 counter inputs of an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleS0CounterSubHandler extends AbstractLcnModuleVariableSubHandler {
|
||||
private static final Pattern PATTERN = Pattern
|
||||
.compile(LcnBindingConstants.ADDRESS_REGEX + "\\.C(?<id>\\d)(?<value>\\d+)");
|
||||
|
||||
public LcnModuleS0CounterSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number)
|
||||
throws LcnException {
|
||||
throw new LcnException("Setting S0 counters is not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) throws LcnException {
|
||||
fireUpdateAndReset(matcher, "", Variable.s0IdToVar(Integer.parseInt(matcher.group("id")) - 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Collections.singleton(PATTERN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
import org.openhab.binding.lcn.internal.common.Variable;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Handles Commands and State changes of thresholds of an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleThresholdSubHandler extends AbstractLcnModuleVariableSubHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(LcnModuleThresholdSubHandler.class);
|
||||
private static final Pattern PATTERN = Pattern
|
||||
.compile(LcnBindingConstants.ADDRESS_REGEX + "\\.T(?<registerId>\\d)(?<thresholdId>\\d)(?<value>\\d+)");
|
||||
private static final Pattern PATTERN_BEFORE_2013 = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX
|
||||
+ "\\.S1(?<value0>\\d{5})(?<value1>\\d{5})(?<value2>\\d{5})(?<value3>\\d{5})(?<value4>\\d{5})(?<hyst>\\d{5})");
|
||||
|
||||
public LcnModuleThresholdSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number)
|
||||
throws LcnException {
|
||||
Variable variable = getVariable(channelGroup, number);
|
||||
try {
|
||||
int relativeChange = getRelativeChange(command, variable);
|
||||
handler.sendPck(PckGenerator.setThresholdRelative(variable, LcnDefs.RelVarRef.CURRENT, relativeChange,
|
||||
info.hasExtendedMeasurementProcessing()));
|
||||
|
||||
// request new value, if the module doesn't send it on itself
|
||||
if (variable.shouldPollStatusAfterCommand(info.getFirmwareVersion())) {
|
||||
info.refreshVariable(variable);
|
||||
}
|
||||
} catch (LcnException e) {
|
||||
// current value unknown for some reason, refresh it in case we come again here
|
||||
info.refreshVariable(variable);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) {
|
||||
IntStream stream;
|
||||
Optional<String> groupSuffix;
|
||||
int registerNumber;
|
||||
if (matcher.pattern() == PATTERN) {
|
||||
int thresholdId = Integer.parseInt(matcher.group("thresholdId")) - 1;
|
||||
registerNumber = Integer.parseInt(matcher.group("registerId")) - 1;
|
||||
stream = IntStream.rangeClosed(thresholdId, thresholdId);
|
||||
groupSuffix = Optional.of("");
|
||||
} else if (matcher.pattern() == PATTERN_BEFORE_2013) {
|
||||
stream = IntStream.range(0, LcnDefs.THRESHOLD_COUNT_BEFORE_2013);
|
||||
groupSuffix = Optional.empty();
|
||||
registerNumber = 0;
|
||||
} else {
|
||||
logger.warn("Unexpected pattern: {}", matcher.pattern());
|
||||
return;
|
||||
}
|
||||
|
||||
stream.forEach(i -> {
|
||||
try {
|
||||
fireUpdateAndReset(matcher, groupSuffix.orElse(String.valueOf(i)),
|
||||
Variable.thrsIdToVar(registerNumber, i));
|
||||
} catch (LcnException e) {
|
||||
logger.warn("Parse error: {}", e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Arrays.asList(PATTERN, PATTERN_BEFORE_2013);
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.subhandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.lcn.internal.LcnBindingConstants;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
import org.openhab.binding.lcn.internal.common.PckGenerator;
|
||||
import org.openhab.binding.lcn.internal.common.Variable;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Handles Commands and State changes of variables of an LCN module.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleVariableSubHandler extends AbstractLcnModuleVariableSubHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(LcnModuleVariableSubHandler.class);
|
||||
private static final Pattern PATTERN = Pattern
|
||||
.compile(LcnBindingConstants.ADDRESS_REGEX + "\\.A(?<id>\\d{3})(?<value>\\d+)");
|
||||
private static final Pattern PATTERN_LEGACY = Pattern
|
||||
.compile(LcnBindingConstants.ADDRESS_REGEX + "\\.(?<value>\\d+)");
|
||||
|
||||
public LcnModuleVariableSubHandler(LcnModuleHandler handler, ModInfo info) {
|
||||
super(handler, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number)
|
||||
throws LcnException {
|
||||
Variable variable = getVariable(channelGroup, number);
|
||||
try {
|
||||
int relativeChange = getRelativeChange(command, variable);
|
||||
handler.sendPck(PckGenerator.setVariableRelative(variable, LcnDefs.RelVarRef.CURRENT, relativeChange));
|
||||
|
||||
// request new value, if the module doesn't send it on itself
|
||||
if (variable.shouldPollStatusAfterCommand(info.getFirmwareVersion())) {
|
||||
info.refreshVariable(variable);
|
||||
}
|
||||
} catch (LcnException e) {
|
||||
// current value unknown for some reason, refresh it in case we come again here
|
||||
info.refreshVariable(variable);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStatusMessage(Matcher matcher) throws LcnException {
|
||||
Variable variable;
|
||||
if (matcher.pattern() == PATTERN) {
|
||||
variable = Variable.varIdToVar(Integer.parseInt(matcher.group("id")) - 1);
|
||||
} else if (matcher.pattern() == PATTERN_LEGACY) {
|
||||
variable = info.getLastRequestedVarWithoutTypeInResponse();
|
||||
info.setLastRequestedVarWithoutTypeInResponse(Variable.UNKNOWN); // Reset
|
||||
} else {
|
||||
logger.warn("Unexpected pattern: {}", matcher.pattern());
|
||||
return;
|
||||
}
|
||||
fireUpdateAndReset(matcher, "", variable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Pattern> getPckStatusMessagePatterns() {
|
||||
return Arrays.asList(PATTERN, PATTERN_LEGACY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="lcn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>LCN Binding</name>
|
||||
<description>This is the binding for Local Control Network (LCN)</description>
|
||||
<author>Fabian Wolter</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
|
||||
https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:lcn:pckGateway">
|
||||
<parameter name="hostname" type="text" required="true">
|
||||
<label>Hostname</label>
|
||||
<description>The hostname or the IP address of the PCK gateway</description>
|
||||
<context>network-address</context>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer" required="true" min="1" max="65535">
|
||||
<label>Port</label>
|
||||
<description>The IP port of the PCK gateway</description>
|
||||
<default>4114</default>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
<parameter name="username" type="text" required="true">
|
||||
<label>Username</label>
|
||||
<description>The login username of the PCK gateway</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="true">
|
||||
<label>Password</label>
|
||||
<description>The login password of the PCK gateway</description>
|
||||
<context>password</context>
|
||||
</parameter>
|
||||
<parameter name="mode" type="text" required="true">
|
||||
<label>Dimming Mode</label>
|
||||
<description><![CDATA[<strong>IMPORTANT:</strong> Dimming range of all modules. Must be the same value as configured in LCN-PRO (Options/Settings/Expert Settings). If you <em>only</em> have modules with firmware newer than Feb. 2013, you probably want to choose 0 - 200.]]></description>
|
||||
<default>native200</default>
|
||||
<options>
|
||||
<option value="native50">0 - 50</option>
|
||||
<option value="native200">0 - 200</option>
|
||||
</options>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
<parameter name="timeoutMs" type="integer" required="true" unit="ms">
|
||||
<label>Connection Timeout</label>
|
||||
<description>Period after which an LCN command is resent, when no acknowledge has been received (in ms).</description>
|
||||
<default>3500</default>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="thing-type:lcn:module">
|
||||
<parameter name="moduleId" type="integer" required="true" min="5" max="254">
|
||||
<label>Module ID</label>
|
||||
<description>The module ID, configured in LCN-PRO</description>
|
||||
</parameter>
|
||||
<parameter name="segmentId" type="integer" required="true" min="0" max="128">
|
||||
<label>Segment ID</label>
|
||||
<description>The segment ID the module is in (0 if no segments are present)</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="thing-type:lcn:group">
|
||||
<parameter name="groupId" type="integer" required="true" min="3" max="254">
|
||||
<label>Group Number</label>
|
||||
<description>The group number, configured in LCN-PRO</description>
|
||||
</parameter>
|
||||
<parameter name="moduleId" type="integer" required="true" min="5" max="254">
|
||||
<label>Module ID</label>
|
||||
<description>The module ID of any module in the group. The state of this module is used for visualization of the
|
||||
group as representative for all modules.</description>
|
||||
</parameter>
|
||||
<parameter name="segmentId" type="integer" required="true" min="0" max="128">
|
||||
<label>Segment ID</label>
|
||||
<description>The segment ID of all modules in this group (0 if no segments are present)</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="channel-type:lcn:variable">
|
||||
<parameter name="unit" type="text" required="true">
|
||||
<label>Unit</label>
|
||||
<description>Unit of the sensor</description>
|
||||
<default>native</default>
|
||||
<options>
|
||||
<option value="native">LCN Native</option>
|
||||
<option value="temperature">Temperature (°C or °F)</option>
|
||||
<option value="light">Light (Lux)</option>
|
||||
<option value="co2">CO₂ (ppm)</option>
|
||||
<option value="power">Power (W)</option>
|
||||
<option value="energy">Energy (kWh)</option>
|
||||
<option value="current">Current (mA)</option>
|
||||
<option value="voltage">Voltage (V)</option>
|
||||
<option value="angle">Angle (°)</option>
|
||||
<option value="windspeed">Windspeed (m/s)</option>
|
||||
</options>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
<parameter name="parameter" type="integer" min="1">
|
||||
<label>Pulses per kWh</label>
|
||||
<description>Only for S0 counters (power or energy)</description>
|
||||
<default>1000</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,174 @@
|
||||
# binding
|
||||
binding.lcn.name = LCN Binding
|
||||
binding.lcn.description = Binding für Local Control Network (LCN)
|
||||
|
||||
# thing types
|
||||
thing-type.lcn.pckGateway.label = LCN-PCK-Koppler
|
||||
thing-type.lcn.pckGateway.description = z.B. die LCN-PCHK-Software oder das Hutschienenmodul LCN-PKE
|
||||
thing-type.lcn.module.label = LCN-Modul
|
||||
thing-type.lcn.module.description = z.B. LCN-UPP, LCN-SH, LCN-HU
|
||||
thing-type.lcn.group.label = LCN-Gruppe
|
||||
thing-type.lcn.group.description = Eine Gruppe mit mehreren Modulen, wie in LCN-PRO parametriert
|
||||
|
||||
# thing type config description
|
||||
thing-type.config.lcn.pckGateway.hostname.description = Hostname oder die IP-Adresse des PCK-Kopplers
|
||||
thing-type.config.lcn.pckGateway.port.description = Netzwerk-Port auf dem der PCK-Koppler läuft
|
||||
thing-type.config.lcn.pckGateway.username.description = Benutzername vom PCK-Koppler
|
||||
thing-type.config.lcn.pckGateway.password.description = Login-Passwort vom PCK-Koppler
|
||||
thing-type.config.lcn.pckGateway.mode.description = <strong>WICHTIG:</strong> Der Dimmbereich von allen LCN-Modulen. Muss der gleiche Wert, wie in LCN-PRO sein (Optionen/Einstellungen/Experteneinstellungen). Wenn <em>nur</em> Module älter als 2013 im Bus vorhanden sind, muss hier wahrscheinlich 0 - 200 ausgewählt werden.
|
||||
thing-type.config.lcn.pckGateway.timeoutMs.description = Zeit nach der eine PCK-Nachricht erneut gesendet wird, wenn vom Modul keine positive Quittung empfangen wurde.
|
||||
|
||||
thing-type.config.lcn.module.moduleId.label = Modul-ID
|
||||
thing-type.config.lcn.module.moduleId.description = Modul-ID, wie in LCN-PRO parametriert
|
||||
thing-type.config.lcn.module.segmentId.label = Segment-ID
|
||||
thing-type.config.lcn.module.segmentId.description = ID des Segments, in dem sich das Modul befindet (0 wenn keine Segmente vorhanden sind)
|
||||
|
||||
thing-type.config.lcn.group.groupId.label = Gruppennummer
|
||||
thing-type.config.lcn.group.groupId.description = Nummer der Gruppe, wie in LCN-PRO parametriert
|
||||
thing-type.config.lcn.group.moduleId.label = Modul-ID eines Moduls aus der Gruppe
|
||||
thing-type.config.lcn.group.moduleId.description = Der Zustand dieses Moduls wird zur Visualisierung der Gruppe, stellvertretend für alle Module, genutzt
|
||||
thing-type.config.lcn.group.segmentId.label = Segment-ID
|
||||
thing-type.config.lcn.group.segmentId.description = Segment-ID in dem sich die Module der Gruppe befinden (0 wenn keine Segmente vorhanden sind)
|
||||
|
||||
# channel type config description
|
||||
channel-type.config.lcn.variable.unit.label = Einheit
|
||||
channel-type.config.lcn.variable.unit.description = Einheit des Sensors
|
||||
channel-type.config.lcn.variable.unit.option.native = LCN-Wert
|
||||
channel-type.config.lcn.variable.unit.option.temperature = Temperatur (°C)
|
||||
channel-type.config.lcn.variable.unit.option.light = Licht (Lux)
|
||||
channel-type.config.lcn.variable.unit.option.co2 = CO\u2082 (ppm)
|
||||
channel-type.config.lcn.variable.unit.option.power = Leistung (W)
|
||||
channel-type.config.lcn.variable.unit.option.energy = Zählerstand (kWh)
|
||||
channel-type.config.lcn.variable.unit.option.current = Strom (mA)
|
||||
channel-type.config.lcn.variable.unit.option.voltage = Spannung (V)
|
||||
channel-type.config.lcn.variable.unit.option.angle = Winkel (°)
|
||||
channel-type.config.lcn.variable.unit.option.windspeed = Windgeschwindigkeit (m/s)
|
||||
channel-type.config.lcn.variable.parameter.label = Impulse pro kWh
|
||||
channel-type.config.lcn.variable.parameter.description = Nur für S0-Zähler
|
||||
|
||||
# channel types
|
||||
channel-group-type.lcn.outputs.label = Ausgänge
|
||||
channel-group-type.lcn.outputs.channel.1.label = Ausgang 1
|
||||
channel-group-type.lcn.outputs.channel.2.label = Ausgang 2
|
||||
channel-group-type.lcn.outputs.channel.3.label = Ausgang 3
|
||||
channel-group-type.lcn.outputs.channel.4.label = Ausgang 4
|
||||
channel-group-type.lcn.outputs.channel.color.label = RGB-Steuerung für Ausgänge 1-3
|
||||
channel-group-type.lcn.rollershutteroutputs.label = Rollläden an Ausgängen
|
||||
channel-group-type.lcn.rollershutteroutputs.channel.1.label = Rollläden an Ausgängen 1+2
|
||||
channel-group-type.lcn.relays.label = Relais
|
||||
channel-group-type.lcn.relays.channel.1.label = Relais 1
|
||||
channel-group-type.lcn.relays.channel.2.label = Relais 2
|
||||
channel-group-type.lcn.relays.channel.3.label = Relais 3
|
||||
channel-group-type.lcn.relays.channel.4.label = Relais 4
|
||||
channel-group-type.lcn.relays.channel.5.label = Relais 5
|
||||
channel-group-type.lcn.relays.channel.6.label = Relais 6
|
||||
channel-group-type.lcn.relays.channel.7.label = Relais 7
|
||||
channel-group-type.lcn.relays.channel.8.label = Relais 8
|
||||
channel-group-type.lcn.rollershutterrelays.label = Rollläden an Relais
|
||||
channel-group-type.lcn.rollershutterrelays.channel.1.label = Rollläden an Relais 1+2
|
||||
channel-group-type.lcn.rollershutterrelays.channel.2.label = Rollläden an Relais 3+4
|
||||
channel-group-type.lcn.rollershutterrelays.channel.3.label = Rollläden an Relais 5+6
|
||||
channel-group-type.lcn.rollershutterrelays.channel.4.label = Rollläden an Relais 7+8
|
||||
channel-group-type.lcn.logics.label = Logik-Funktionen
|
||||
channel-group-type.lcn.logics.channel.1.label = Logik-Funktion 1
|
||||
channel-group-type.lcn.logics.channel.2.label = Logik-Funktion 2
|
||||
channel-group-type.lcn.logics.channel.3.label = Logik-Funktion 3
|
||||
channel-group-type.lcn.logics.channel.4.label = Logik-Funktion 4
|
||||
channel-group-type.lcn.binarysensors.label = Binärsensoren
|
||||
channel-group-type.lcn.binarysensors.channel.1.label = Binärsensor 1
|
||||
channel-group-type.lcn.binarysensors.channel.2.label = Binärsensor 2
|
||||
channel-group-type.lcn.binarysensors.channel.3.label = Binärsensor 3
|
||||
channel-group-type.lcn.binarysensors.channel.4.label = Binärsensor 4
|
||||
channel-group-type.lcn.binarysensors.channel.5.label = Binärsensor 5
|
||||
channel-group-type.lcn.binarysensors.channel.6.label = Binärsensor 6
|
||||
channel-group-type.lcn.binarysensors.channel.7.label = Binärsensor 7
|
||||
channel-group-type.lcn.binarysensors.channel.8.label = Binärsensor 8
|
||||
channel-group-type.lcn.variables.label = Variablen
|
||||
channel-group-type.lcn.variables.channel.1.label = Variable 1
|
||||
channel-group-type.lcn.variables.channel.2.label = Variable 2
|
||||
channel-group-type.lcn.variables.channel.3.label = Variable 3
|
||||
channel-group-type.lcn.variables.channel.4.label = Variable 4
|
||||
channel-group-type.lcn.variables.channel.5.label = Variable 5
|
||||
channel-group-type.lcn.variables.channel.6.label = Variable 6
|
||||
channel-group-type.lcn.variables.channel.7.label = Variable 7
|
||||
channel-group-type.lcn.variables.channel.8.label = Variable 8
|
||||
channel-group-type.lcn.variables.channel.9.label = Variable 9
|
||||
channel-group-type.lcn.variables.channel.10.label = Variable 10
|
||||
channel-group-type.lcn.variables.channel.11.label = Variable 11
|
||||
channel-group-type.lcn.variables.channel.12.label = Variable 12
|
||||
channel-group-type.lcn.rvarsetpoints.label = Regler
|
||||
channel-group-type.lcn.rvarsetpoints.channel.1.label = Regler 1 Sollwert
|
||||
channel-group-type.lcn.rvarsetpoints.channel.2.label = Regler 2 Sollwert
|
||||
channel-group-type.lcn.rvarlocks.label = Regler Sperren
|
||||
channel-group-type.lcn.rvarlocks.channel.1.label = Regler 1 Sperre
|
||||
channel-group-type.lcn.rvarlocks.channel.2.label = Regler 2 Sperre
|
||||
channel-group-type.lcn.thresholdregisters1.label = Schwellwertregister 1
|
||||
channel-group-type.lcn.thresholdregisters1.channel.1.label = Schwellwert 1
|
||||
channel-group-type.lcn.thresholdregisters1.channel.2.label = Schwellwert 2
|
||||
channel-group-type.lcn.thresholdregisters1.channel.3.label = Schwellwert 3
|
||||
channel-group-type.lcn.thresholdregisters1.channel.4.label = Schwellwert 4
|
||||
channel-group-type.lcn.thresholdregisters1.channel.5.label = Schwellwert 5
|
||||
channel-group-type.lcn.thresholdregisters2.label = Schwellwertregister 2
|
||||
channel-group-type.lcn.thresholdregisters2.channel.1.label = Schwellwert 1
|
||||
channel-group-type.lcn.thresholdregisters2.channel.2.label = Schwellwert 2
|
||||
channel-group-type.lcn.thresholdregisters2.channel.3.label = Schwellwert 3
|
||||
channel-group-type.lcn.thresholdregisters2.channel.4.label = Schwellwert 4
|
||||
channel-group-type.lcn.thresholdregisters3.label = Schwellwertregister 3
|
||||
channel-group-type.lcn.thresholdregisters3.channel.1.label = Schwellwert 1
|
||||
channel-group-type.lcn.thresholdregisters3.channel.2.label = Schwellwert 2
|
||||
channel-group-type.lcn.thresholdregisters3.channel.3.label = Schwellwert 3
|
||||
channel-group-type.lcn.thresholdregisters3.channel.4.label = Schwellwert 4
|
||||
channel-group-type.lcn.thresholdregisters4.label = Schwellwertregister 4
|
||||
channel-group-type.lcn.thresholdregisters4.channel.1.label = Schwellwert 1
|
||||
channel-group-type.lcn.thresholdregisters4.channel.2.label = Schwellwert 2
|
||||
channel-group-type.lcn.thresholdregisters4.channel.3.label = Schwellwert 3
|
||||
channel-group-type.lcn.thresholdregisters4.channel.4.label = Schwellwert 4
|
||||
channel-group-type.lcn.s0inputs.label = S0-Zähler
|
||||
channel-group-type.lcn.s0inputs.channel.1.label = S0-Zähler 1
|
||||
channel-group-type.lcn.s0inputs.channel.2.label = S0-Zähler 2
|
||||
channel-group-type.lcn.s0inputs.channel.3.label = S0-Zähler 3
|
||||
channel-group-type.lcn.s0inputs.channel.4.label = S0-Zähler 4
|
||||
channel-group-type.lcn.keyslocktablea.label = Tastensperren Tabelle A
|
||||
channel-group-type.lcn.keyslocktablea.channel.1.label = A1 Sperre
|
||||
channel-group-type.lcn.keyslocktablea.channel.2.label = A2 Sperre
|
||||
channel-group-type.lcn.keyslocktablea.channel.3.label = A3 Sperre
|
||||
channel-group-type.lcn.keyslocktablea.channel.4.label = A4 Sperre
|
||||
channel-group-type.lcn.keyslocktablea.channel.5.label = A5 Sperre
|
||||
channel-group-type.lcn.keyslocktablea.channel.6.label = A6 Sperre
|
||||
channel-group-type.lcn.keyslocktablea.channel.7.label = A7 Sperre
|
||||
channel-group-type.lcn.keyslocktablea.channel.8.label = A8 Sperre
|
||||
channel-group-type.lcn.keyslocktableb.label = Tastensperren Tabelle B
|
||||
channel-group-type.lcn.keyslocktableb.channel.1.label = B1 Sperre
|
||||
channel-group-type.lcn.keyslocktableb.channel.2.label = B2 Sperre
|
||||
channel-group-type.lcn.keyslocktableb.channel.3.label = B3 Sperre
|
||||
channel-group-type.lcn.keyslocktableb.channel.4.label = B4 Sperre
|
||||
channel-group-type.lcn.keyslocktableb.channel.5.label = B5 Sperre
|
||||
channel-group-type.lcn.keyslocktableb.channel.6.label = B6 Sperre
|
||||
channel-group-type.lcn.keyslocktableb.channel.7.label = B7 Sperre
|
||||
channel-group-type.lcn.keyslocktableb.channel.8.label = B8 Sperre
|
||||
channel-group-type.lcn.keyslocktablec.label = Tastensperren Tabelle C
|
||||
channel-group-type.lcn.keyslocktablec.channel.1.label = C1 Sperre
|
||||
channel-group-type.lcn.keyslocktablec.channel.2.label = C2 Sperre
|
||||
channel-group-type.lcn.keyslocktablec.channel.3.label = C3 Sperre
|
||||
channel-group-type.lcn.keyslocktablec.channel.4.label = C4 Sperre
|
||||
channel-group-type.lcn.keyslocktablec.channel.5.label = C5 Sperre
|
||||
channel-group-type.lcn.keyslocktablec.channel.6.label = C6 Sperre
|
||||
channel-group-type.lcn.keyslocktablec.channel.7.label = C7 Sperre
|
||||
channel-group-type.lcn.keyslocktablec.channel.8.label = C8 Sperre
|
||||
channel-group-type.lcn.keyslocktabled.label = Tastensperren Tabelle D
|
||||
channel-group-type.lcn.keyslocktabled.channel.1.label = D1 Sperre
|
||||
channel-group-type.lcn.keyslocktabled.channel.2.label = D2 Sperre
|
||||
channel-group-type.lcn.keyslocktabled.channel.3.label = D3 Sperre
|
||||
channel-group-type.lcn.keyslocktabled.channel.4.label = D4 Sperre
|
||||
channel-group-type.lcn.keyslocktabled.channel.5.label = D5 Sperre
|
||||
channel-group-type.lcn.keyslocktabled.channel.6.label = D6 Sperre
|
||||
channel-group-type.lcn.keyslocktabled.channel.7.label = D7 Sperre
|
||||
channel-group-type.lcn.keyslocktabled.channel.8.label = D8 Sperre
|
||||
channel-group-type.lcn.codes.label = Transponder & Fernbedienungen
|
||||
channel-group-type.lcn.codes.channel.transponder.label = Transponder-Code
|
||||
channel-group-type.lcn.codes.channel.fingerprint.label = Fingerprint-Code
|
||||
channel-group-type.lcn.codes.channel.remotecontrolkey.label = Fernbedienung Tasten
|
||||
channel-group-type.lcn.codes.channel.remotecontrolcode.label = Fernbedienung Tasten mit Zutrittscode
|
||||
channel-group-type.lcn.codes.channel.remotecontrolbatterylow.label = Fernbedienung Batterie leer
|
||||
channel-group-type.lcn.hostcommands.label = Kommandos an Host (openHAB)
|
||||
channel-group-type.lcn.hostcommands.channel.sendKeys.label = Sende Tasten
|
||||
@@ -0,0 +1,748 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="lcn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<bridge-type id="pckGateway">
|
||||
<label>LCN-PCHK Gateway</label>
|
||||
<description>An LCN gateway speaking the PCK language. E.g. LCN-PCHK software or the DIN rail device LCN-PKE.</description>
|
||||
|
||||
<config-description-ref uri="thing-type:lcn:pckGateway"/>
|
||||
</bridge-type>
|
||||
|
||||
<thing-type id="module">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="pckGateway"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>LCN Module</label>
|
||||
<description>An LCN bus module, e.g. LCN-UPP, LCN-SH, LCN-HU</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group typeId="outputs" id="output"/>
|
||||
<channel-group typeId="rollershutteroutputs" id="rollershutteroutput"/>
|
||||
<channel-group typeId="leds" id="led"/>
|
||||
<channel-group typeId="relays" id="relay"/>
|
||||
<channel-group typeId="rollershutterrelays" id="rollershutterrelay"/>
|
||||
<channel-group typeId="logics" id="logic"/>
|
||||
<channel-group typeId="binarysensors" id="binarysensor"/>
|
||||
<channel-group typeId="variables" id="variable"/>
|
||||
<channel-group typeId="rvarsetpoints" id="rvarsetpoint"/>
|
||||
<channel-group typeId="rvarlocks" id="rvarlock"/>
|
||||
<channel-group typeId="thresholdregisters1" id="thresholdregister1"/>
|
||||
<channel-group typeId="thresholdregisters2" id="thresholdregister2"/>
|
||||
<channel-group typeId="thresholdregisters3" id="thresholdregister3"/>
|
||||
<channel-group typeId="thresholdregisters4" id="thresholdregister4"/>
|
||||
<channel-group typeId="s0inputs" id="s0input"/>
|
||||
<channel-group typeId="keyslocktablea" id="keylocktablea"/>
|
||||
<channel-group typeId="keyslocktableb" id="keylocktableb"/>
|
||||
<channel-group typeId="keyslocktablec" id="keylocktablec"/>
|
||||
<channel-group typeId="keyslocktabled" id="keylocktabled"/>
|
||||
<channel-group typeId="codes" id="code"/>
|
||||
<channel-group typeId="hostcommands" id="hostcommand"/>
|
||||
</channel-groups>
|
||||
<properties>
|
||||
<property name="serialNumber"/>
|
||||
</properties>
|
||||
<representation-property>serialNumber</representation-property>
|
||||
|
||||
<config-description-ref uri="thing-type:lcn:module"/>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="group">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="pckGateway"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>LCN Group</label>
|
||||
<description>An LCN group with multiple modules, configured in LCN-PRO</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group typeId="outputs" id="output"/>
|
||||
<channel-group typeId="rollershutteroutputs" id="rollershutteroutput"/>
|
||||
<channel-group typeId="leds" id="led"/>
|
||||
<channel-group typeId="relays" id="relay"/>
|
||||
<channel-group typeId="rollershutterrelays" id="rollershutterrelay"/>
|
||||
<channel-group typeId="variables" id="variable"/>
|
||||
<channel-group typeId="rvarsetpoints" id="rvarsetpoint"/>
|
||||
<channel-group typeId="rvarlocks" id="rvarlock"/>
|
||||
<channel-group typeId="thresholdregisters1" id="thresholdregister1"/>
|
||||
<channel-group typeId="thresholdregisters2" id="thresholdregister2"/>
|
||||
<channel-group typeId="thresholdregisters3" id="thresholdregister3"/>
|
||||
<channel-group typeId="thresholdregisters4" id="thresholdregister4"/>
|
||||
<channel-group typeId="keyslocktablea" id="keylocktablea"/>
|
||||
<channel-group typeId="keyslocktableb" id="keylocktableb"/>
|
||||
<channel-group typeId="keyslocktablec" id="keylocktablec"/>
|
||||
<channel-group typeId="keyslocktabled" id="keylocktabled"/>
|
||||
</channel-groups>
|
||||
|
||||
<config-description-ref uri="thing-type:lcn:group"/>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="output" advanced="true">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Output</label>
|
||||
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="color" advanced="true">
|
||||
<item-type>Color</item-type>
|
||||
<label>Color</label>
|
||||
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||
</channel-type>
|
||||
|
||||
<channel-group-type id="outputs">
|
||||
<label>Dimmer Outputs</label>
|
||||
<channels>
|
||||
<channel typeId="output" id="1">
|
||||
<label>Output 1</label>
|
||||
</channel>
|
||||
<channel typeId="output" id="2">
|
||||
<label>Output 2</label>
|
||||
</channel>
|
||||
<channel typeId="output" id="3">
|
||||
<label>Output 3</label>
|
||||
</channel>
|
||||
<channel typeId="output" id="4">
|
||||
<label>Output 4</label>
|
||||
</channel>
|
||||
<channel typeId="color" id="color">
|
||||
<label>RGB Color Control (Outputs 1-3)</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="relay" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Relay</label>
|
||||
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||
</channel-type>
|
||||
|
||||
<channel-group-type id="relays">
|
||||
<label>Relays</label>
|
||||
<channels>
|
||||
<channel typeId="relay" id="1">
|
||||
<label>Relay 1</label>
|
||||
</channel>
|
||||
<channel typeId="relay" id="2">
|
||||
<label>Relay 2</label>
|
||||
</channel>
|
||||
<channel typeId="relay" id="3">
|
||||
<label>Relay 3</label>
|
||||
</channel>
|
||||
<channel typeId="relay" id="4">
|
||||
<label>Relay 4</label>
|
||||
</channel>
|
||||
<channel typeId="relay" id="5">
|
||||
<label>Relay 5</label>
|
||||
</channel>
|
||||
<channel typeId="relay" id="6">
|
||||
<label>Relay 6</label>
|
||||
</channel>
|
||||
<channel typeId="relay" id="7">
|
||||
<label>Relay 7</label>
|
||||
</channel>
|
||||
<channel typeId="relay" id="8">
|
||||
<label>Relay 8</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="rollershutter" advanced="true">
|
||||
<item-type>Rollershutter</item-type>
|
||||
<label>Roller Shutter</label>
|
||||
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||
<config-description>
|
||||
<parameter name="invertUpDown" type="boolean">
|
||||
<label>Invert Up/Down</label>
|
||||
<description>According LCN spec., the Up wire is connected to the "normally open" contact/Output 1. Use this
|
||||
parameter to invert that logic.</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</channel-type>
|
||||
|
||||
<channel-group-type id="rollershutterrelays">
|
||||
<label>Roller Shutter on Relays</label>
|
||||
<channels>
|
||||
<channel typeId="rollershutter" id="1">
|
||||
<label>Shutter 1-2</label>
|
||||
</channel>
|
||||
<channel typeId="rollershutter" id="2">
|
||||
<label>Shutter 3-4</label>
|
||||
</channel>
|
||||
<channel typeId="rollershutter" id="3">
|
||||
<label>Shutter 5-6</label>
|
||||
</channel>
|
||||
<channel typeId="rollershutter" id="4">
|
||||
<label>Shutter 7-8</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="rollershutteroutputs">
|
||||
<label>Roller Shutter on Dimmer Outputs</label>
|
||||
<channels>
|
||||
<channel typeId="rollershutter" id="1">
|
||||
<label>Shutter 1-2</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="led" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>LED</label>
|
||||
<state>
|
||||
<options>
|
||||
<option value="OFF">Off</option>
|
||||
<option value="ON">On</option>
|
||||
<option value="BLINK">Blink</option>
|
||||
<option value="FLICKER">Flicker</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-group-type id="leds">
|
||||
<label>LEDs</label>
|
||||
<channels>
|
||||
<channel typeId="led" id="1">
|
||||
<label>LED 1</label>
|
||||
</channel>
|
||||
<channel typeId="led" id="2">
|
||||
<label>LED 2</label>
|
||||
</channel>
|
||||
<channel typeId="led" id="3">
|
||||
<label>LED 3</label>
|
||||
</channel>
|
||||
<channel typeId="led" id="4">
|
||||
<label>LED 4</label>
|
||||
</channel>
|
||||
<channel typeId="led" id="5">
|
||||
<label>LED 5</label>
|
||||
</channel>
|
||||
<channel typeId="led" id="6">
|
||||
<label>LED 6</label>
|
||||
</channel>
|
||||
<channel typeId="led" id="7">
|
||||
<label>LED 7</label>
|
||||
</channel>
|
||||
<channel typeId="led" id="8">
|
||||
<label>LED 8</label>
|
||||
</channel>
|
||||
<channel typeId="led" id="9">
|
||||
<label>LED 9</label>
|
||||
</channel>
|
||||
<channel typeId="led" id="10">
|
||||
<label>LED 10</label>
|
||||
</channel>
|
||||
<channel typeId="led" id="11">
|
||||
<label>LED 11</label>
|
||||
</channel>
|
||||
<channel typeId="led" id="12">
|
||||
<label>LED 12</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="logic" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Logic Operation</label>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="NOT">Not (not fulfilled)</option>
|
||||
<option value="OR">Or (partly fulfilled)</option>
|
||||
<option value="AND">And (fulfilled)</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-group-type id="logics">
|
||||
<label>Logic Operations</label>
|
||||
<channels>
|
||||
<channel typeId="logic" id="1">
|
||||
<label>Logic Operation 1</label>
|
||||
</channel>
|
||||
<channel typeId="logic" id="2">
|
||||
<label>Logic Operation 2</label>
|
||||
</channel>
|
||||
<channel typeId="logic" id="3">
|
||||
<label>Logic Operation 3</label>
|
||||
</channel>
|
||||
<channel typeId="logic" id="4">
|
||||
<label>Logic Operation 4</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="binarysensor" advanced="true">
|
||||
<item-type>Contact</item-type>
|
||||
<label>Binary Sensor</label>
|
||||
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||
<config-description>
|
||||
<parameter name="invertState" type="boolean">
|
||||
<label>Invert State</label>
|
||||
<description>Per default a binary sensor state 0 corresponds to "closed" while 1 corresponds to "open". Use this
|
||||
parameter to invert that logic.</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</channel-type>
|
||||
|
||||
<channel-group-type id="binarysensors">
|
||||
<label>Binary Sensors</label>
|
||||
<channels>
|
||||
<channel typeId="binarysensor" id="1">
|
||||
<label>Binary Sensor 1</label>
|
||||
</channel>
|
||||
<channel typeId="binarysensor" id="2">
|
||||
<label>Binary Sensor 2</label>
|
||||
</channel>
|
||||
<channel typeId="binarysensor" id="3">
|
||||
<label>Binary Sensor 3</label>
|
||||
</channel>
|
||||
<channel typeId="binarysensor" id="4">
|
||||
<label>Binary Sensor 4</label>
|
||||
</channel>
|
||||
<channel typeId="binarysensor" id="5">
|
||||
<label>Binary Sensor 5</label>
|
||||
</channel>
|
||||
<channel typeId="binarysensor" id="6">
|
||||
<label>Binary Sensor 6</label>
|
||||
</channel>
|
||||
<channel typeId="binarysensor" id="7">
|
||||
<label>Binary Sensor 7</label>
|
||||
</channel>
|
||||
<channel typeId="binarysensor" id="8">
|
||||
<label>Binary Sensor 8</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="variable" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Variable</label>
|
||||
<config-description-ref uri="channel-type:lcn:variable"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-group-type id="variables">
|
||||
<label>Variables</label>
|
||||
<channels>
|
||||
<channel typeId="variable" id="1">
|
||||
<label>Variable 1 or TVar</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="2">
|
||||
<label>Variable 2</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="3">
|
||||
<label>Variable 3</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="4">
|
||||
<label>Variable 4</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="5">
|
||||
<label>Variable 5</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="6">
|
||||
<label>Variable 6</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="7">
|
||||
<label>Variable 7</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="8">
|
||||
<label>Variable 8</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="9">
|
||||
<label>Variable 9</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="10">
|
||||
<label>Variable 10</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="11">
|
||||
<label>Variable 11</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="12">
|
||||
<label>Variable 12</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="rvarsetpoints">
|
||||
<label>RVar Setpoints</label>
|
||||
<channels>
|
||||
<channel typeId="variable" id="1">
|
||||
<label>R1Var Setpoint</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="2">
|
||||
<label>R2Var Setpoint</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="rvarlock" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>RVar Lock State</label>
|
||||
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||
</channel-type>
|
||||
|
||||
<channel-group-type id="rvarlocks">
|
||||
<label>RVar Lock State</label>
|
||||
<channels>
|
||||
<channel typeId="rvarlock" id="1">
|
||||
<label>R1Var Lock</label>
|
||||
</channel>
|
||||
<channel typeId="rvarlock" id="2">
|
||||
<label>R2Var Lock</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="thresholdregisters1">
|
||||
<label>Threshold Register 1</label>
|
||||
<channels>
|
||||
<channel typeId="variable" id="1">
|
||||
<label>Threshold 1</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="2">
|
||||
<label>Threshold 2</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="3">
|
||||
<label>Threshold 3</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="4">
|
||||
<label>Threshold 4</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="5">
|
||||
<label>Threshold 5</label>
|
||||
<description>Only before Feb. 2013</description>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="thresholdregisters2">
|
||||
<label>Threshold Register 2</label>
|
||||
<channels>
|
||||
<channel typeId="variable" id="1">
|
||||
<label>Threshold 1</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="2">
|
||||
<label>Threshold 2</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="3">
|
||||
<label>Threshold 3</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="4">
|
||||
<label>Threshold 4</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="thresholdregisters3">
|
||||
<label>Threshold Register 3</label>
|
||||
<channels>
|
||||
<channel typeId="variable" id="1">
|
||||
<label>Threshold 1</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="2">
|
||||
<label>Threshold 2</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="3">
|
||||
<label>Threshold 3</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="4">
|
||||
<label>Threshold 4</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="thresholdregisters4">
|
||||
<label>Threshold Register 4</label>
|
||||
<channels>
|
||||
<channel typeId="variable" id="1">
|
||||
<label>Threshold 1</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="2">
|
||||
<label>Threshold 2</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="3">
|
||||
<label>Threshold 3</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="4">
|
||||
<label>Threshold 4</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="s0inputs">
|
||||
<label>S0 Counters</label>
|
||||
<channels>
|
||||
<channel typeId="variable" id="1">
|
||||
<label>S0 Counter 1</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="2">
|
||||
<label>S0 Counter 2</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="3">
|
||||
<label>S0 Counter 3</label>
|
||||
</channel>
|
||||
<channel typeId="variable" id="4">
|
||||
<label>S0 Counter 4</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="keylock" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Keys Lock State</label>
|
||||
</channel-type>
|
||||
|
||||
<channel-group-type id="keyslocktablea">
|
||||
<label>Keys Lock State of Table A</label>
|
||||
<channels>
|
||||
<channel typeId="keylock" id="1">
|
||||
<label>Key 1</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="2">
|
||||
<label>Key 2</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="3">
|
||||
<label>Key 3</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="4">
|
||||
<label>Key 4</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="5">
|
||||
<label>Key 5</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="6">
|
||||
<label>Key 6</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="7">
|
||||
<label>Key 7</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="8">
|
||||
<label>Key 8</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="keyslocktableb">
|
||||
<label>Keys Lock State of Table B</label>
|
||||
<channels>
|
||||
<channel typeId="keylock" id="1">
|
||||
<label>Key 1</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="2">
|
||||
<label>Key 2</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="3">
|
||||
<label>Key 3</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="4">
|
||||
<label>Key 4</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="5">
|
||||
<label>Key 5</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="6">
|
||||
<label>Key 6</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="7">
|
||||
<label>Key 7</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="8">
|
||||
<label>Key 8</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="keyslocktablec">
|
||||
<label>Keys Lock State of Table C</label>
|
||||
<channels>
|
||||
<channel typeId="keylock" id="1">
|
||||
<label>Key 1</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="2">
|
||||
<label>Key 2</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="3">
|
||||
<label>Key 3</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="4">
|
||||
<label>Key 4</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="5">
|
||||
<label>Key 5</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="6">
|
||||
<label>Key 6</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="7">
|
||||
<label>Key 7</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="8">
|
||||
<label>Key 8</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="keyslocktabled">
|
||||
<label>Keys Lock State of Table D</label>
|
||||
<channels>
|
||||
<channel typeId="keylock" id="1">
|
||||
<label>Key 1</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="2">
|
||||
<label>Key 2</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="3">
|
||||
<label>Key 3</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="4">
|
||||
<label>Key 4</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="5">
|
||||
<label>Key 5</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="6">
|
||||
<label>Key 6</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="7">
|
||||
<label>Key 7</label>
|
||||
</channel>
|
||||
<channel typeId="keylock" id="8">
|
||||
<label>Key 8</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="codes">
|
||||
<label>Transponder & Remote Control</label>
|
||||
<channels>
|
||||
<channel typeId="transponders" id="transponder"/>
|
||||
<channel typeId="fingerprints" id="fingerprint"/>
|
||||
<channel typeId="remotecontrolkeys" id="remotecontrolkey"/>
|
||||
<channel typeId="remotecontrolcodes" id="remotecontrolcode"/>
|
||||
<channel typeId="remotecontrolsbatterylow" id="remotecontrolbatterylow"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="transponders" advanced="true">
|
||||
<kind>trigger</kind>
|
||||
<label>Transponder Codes</label>
|
||||
<event/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="fingerprints" advanced="true">
|
||||
<kind>trigger</kind>
|
||||
<label>Fingerprint Codes</label>
|
||||
<event/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="remotecontrolkeys" advanced="true">
|
||||
<kind>trigger</kind>
|
||||
<label>Remote Control Keys</label>
|
||||
<event/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="remotecontrolcodes" advanced="true">
|
||||
<kind>trigger</kind>
|
||||
<label>Remote Control with Access Control Code</label>
|
||||
<event/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="remotecontrolsbatterylow" advanced="true">
|
||||
<kind>trigger</kind>
|
||||
<label>Remote Control Low Battery</label>
|
||||
<event/>
|
||||
</channel-type>
|
||||
|
||||
<channel-group-type id="hostcommands">
|
||||
<label>Host Command (to openHAB)</label>
|
||||
<channels>
|
||||
<channel typeId="sendKeys" id="sendKeys"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="sendKeys" advanced="true">
|
||||
<kind>trigger</kind>
|
||||
<label>Send Keys</label>
|
||||
<event>
|
||||
<options>
|
||||
<option value="A1:HIT">A1 Hit</option>
|
||||
<option value="A1:MAKE">A1 Make</option>
|
||||
<option value="A1:BREAK">A1 Break</option>
|
||||
<option value="A2:HIT">A2 Hit</option>
|
||||
<option value="A2:MAKE">A2 Make</option>
|
||||
<option value="A2:BREAK">A2 Break</option>
|
||||
<option value="A3:HIT">A3 Hit</option>
|
||||
<option value="A3:MAKE">A3 Make</option>
|
||||
<option value="A3:BREAK">A3 Break</option>
|
||||
<option value="A4:HIT">A4 Hit</option>
|
||||
<option value="A4:MAKE">A4 Make</option>
|
||||
<option value="A4:BREAK">A4 Break</option>
|
||||
<option value="A5:HIT">A5 Hit</option>
|
||||
<option value="A5:MAKE">A5 Make</option>
|
||||
<option value="A5:BREAK">A5 Break</option>
|
||||
<option value="A6:HIT">A6 Hit</option>
|
||||
<option value="A6:MAKE">A6 Make</option>
|
||||
<option value="A6:BREAK">A6 Break</option>
|
||||
<option value="A7:HIT">A7 Hit</option>
|
||||
<option value="A7:MAKE">A7 Make</option>
|
||||
<option value="A7:BREAK">A7 Break</option>
|
||||
<option value="A8:HIT">A8 Hit</option>
|
||||
<option value="A8:MAKE">A8 Make</option>
|
||||
<option value="A8:BREAK">A8 Break</option>
|
||||
|
||||
<option value="B1:HIT">B1 Hit</option>
|
||||
<option value="B1:MAKE">B1 Make</option>
|
||||
<option value="B1:BREAK">B1 Break</option>
|
||||
<option value="B2:HIT">B2 Hit</option>
|
||||
<option value="B2:MAKE">B2 Make</option>
|
||||
<option value="B2:BREAK">B2 Break</option>
|
||||
<option value="B3:HIT">B3 Hit</option>
|
||||
<option value="B3:MAKE">B3 Make</option>
|
||||
<option value="B3:BREAK">B3 Break</option>
|
||||
<option value="B4:HIT">B4 Hit</option>
|
||||
<option value="B4:MAKE">B4 Make</option>
|
||||
<option value="B4:BREAK">B4 Break</option>
|
||||
<option value="B5:HIT">B5 Hit</option>
|
||||
<option value="B5:MAKE">B5 Make</option>
|
||||
<option value="B5:BREAK">B5 Break</option>
|
||||
<option value="B6:HIT">B6 Hit</option>
|
||||
<option value="B6:MAKE">B6 Make</option>
|
||||
<option value="B6:BREAK">B6 Break</option>
|
||||
<option value="B7:HIT">B7 Hit</option>
|
||||
<option value="B7:MAKE">B7 Make</option>
|
||||
<option value="B7:BREAK">B7 Break</option>
|
||||
<option value="B8:HIT">B8 Hit</option>
|
||||
<option value="B8:MAKE">B8 Make</option>
|
||||
<option value="B8:BREAK">B8 Break</option>
|
||||
|
||||
<option value="C1:HIT">C1 Hit</option>
|
||||
<option value="C1:MAKE">C1 Make</option>
|
||||
<option value="C1:BREAK">C1 Break</option>
|
||||
<option value="C2:HIT">C2 Hit</option>
|
||||
<option value="C2:MAKE">C2 Make</option>
|
||||
<option value="C2:BREAK">C2 Break</option>
|
||||
<option value="C3:HIT">C3 Hit</option>
|
||||
<option value="C3:MAKE">C3 Make</option>
|
||||
<option value="C3:BREAK">C3 Break</option>
|
||||
<option value="C4:HIT">C4 Hit</option>
|
||||
<option value="C4:MAKE">C4 Make</option>
|
||||
<option value="C4:BREAK">C4 Break</option>
|
||||
<option value="C5:HIT">C5 Hit</option>
|
||||
<option value="C5:MAKE">C5 Make</option>
|
||||
<option value="C5:BREAK">C5 Break</option>
|
||||
<option value="C6:HIT">C6 Hit</option>
|
||||
<option value="C6:MAKE">C6 Make</option>
|
||||
<option value="C6:BREAK">C6 Break</option>
|
||||
<option value="C7:HIT">C7 Hit</option>
|
||||
<option value="C7:MAKE">C7 Make</option>
|
||||
<option value="C7:BREAK">C7 Break</option>
|
||||
<option value="C8:HIT">C8 Hit</option>
|
||||
<option value="C8:MAKE">C8 Make</option>
|
||||
<option value="C8:BREAK">C8 Break</option>
|
||||
</options>
|
||||
</event>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.openhab.binding.lcn.internal.common.LcnDefs;
|
||||
import org.openhab.binding.lcn.internal.common.LcnException;
|
||||
|
||||
/**
|
||||
* Test class for {@link LcnModuleActions}.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ModuleActionsTest {
|
||||
private LcnModuleActions a = new LcnModuleActions();
|
||||
private final LcnModuleHandler handler = mock(LcnModuleHandler.class);
|
||||
@Captor
|
||||
private @NonNullByDefault({}) ArgumentCaptor<byte[]> byteBufferCaptor;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
a = new LcnModuleActions();
|
||||
a.setThingHandler(handler);
|
||||
}
|
||||
|
||||
private byte[] stringToByteBuffer(String string) {
|
||||
return string.getBytes(LcnDefs.LCN_ENCODING);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendDynamicText1CharRow1() throws LcnException {
|
||||
a.sendDynamicText(1, "a");
|
||||
|
||||
verify(handler).sendPck(stringToByteBuffer("GTDT11a\0\0\0\0\0\0\0\0\0\0\0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendDynamicText1ChunkRow1() throws LcnException {
|
||||
a.sendDynamicText(1, "abcdfghijklm");
|
||||
|
||||
verify(handler).sendPck(stringToByteBuffer("GTDT11abcdfghijklm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendDynamicText1Chunk1CharRow1() throws LcnException {
|
||||
a.sendDynamicText(1, "abcdfghijklmn");
|
||||
|
||||
verify(handler, times(2)).sendPck(byteBufferCaptor.capture());
|
||||
|
||||
assertThat(byteBufferCaptor.getAllValues(), contains(stringToByteBuffer("GTDT11abcdfghijklm"),
|
||||
stringToByteBuffer("GTDT12n\0\0\0\0\0\0\0\0\0\0\0")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendDynamicText5ChunksRow1() throws LcnException {
|
||||
a.sendDynamicText(1, "abcdfghijklmnopqrstuvwxyzabcdfghijklmnopqrstuvwxyzabcdfghijk");
|
||||
|
||||
verify(handler, times(5)).sendPck(byteBufferCaptor.capture());
|
||||
|
||||
assertThat(byteBufferCaptor.getAllValues(),
|
||||
containsInAnyOrder(stringToByteBuffer("GTDT11abcdfghijklm"), stringToByteBuffer("GTDT12nopqrstuvwxy"),
|
||||
stringToByteBuffer("GTDT13zabcdfghijkl"), stringToByteBuffer("GTDT14mnopqrstuvwx"),
|
||||
stringToByteBuffer("GTDT15yzabcdfghijk")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendDynamicText5Chunks1CharRow1Truncated() throws LcnException {
|
||||
a.sendDynamicText(1, "abcdfghijklmnopqrstuvwxyzabcdfghijklmnopqrstuvwxyzabcdfghijkl");
|
||||
|
||||
verify(handler, times(5)).sendPck(byteBufferCaptor.capture());
|
||||
|
||||
assertThat(byteBufferCaptor.getAllValues(),
|
||||
containsInAnyOrder(stringToByteBuffer("GTDT11abcdfghijklm"), stringToByteBuffer("GTDT12nopqrstuvwxy"),
|
||||
stringToByteBuffer("GTDT13zabcdfghijkl"), stringToByteBuffer("GTDT14mnopqrstuvwx"),
|
||||
stringToByteBuffer("GTDT15yzabcdfghijk")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendDynamicText5Chunks1UmlautRow1Truncated() throws LcnException {
|
||||
a.sendDynamicText(1, "äcdfghijklmnopqrstuvwxyzabcdfghijklmnopqrstuvwxyzabcdfghijkl");
|
||||
|
||||
verify(handler, times(5)).sendPck(byteBufferCaptor.capture());
|
||||
|
||||
assertThat(byteBufferCaptor.getAllValues(),
|
||||
containsInAnyOrder(stringToByteBuffer("GTDT11äcdfghijklm"), stringToByteBuffer("GTDT12nopqrstuvwxy"),
|
||||
stringToByteBuffer("GTDT13zabcdfghijkl"), stringToByteBuffer("GTDT14mnopqrstuvwx"),
|
||||
stringToByteBuffer("GTDT15yzabcdfghijk")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendDynamicTextRow4() throws LcnException {
|
||||
a.sendDynamicText(4, "abcdfghijklmn");
|
||||
|
||||
verify(handler, times(2)).sendPck(byteBufferCaptor.capture());
|
||||
|
||||
assertThat(byteBufferCaptor.getAllValues(), contains(stringToByteBuffer("GTDT41abcdfghijklm"),
|
||||
stringToByteBuffer("GTDT42n\0\0\0\0\0\0\0\0\0\0\0")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendDynamicTextSplitInCharacter() throws LcnException {
|
||||
a.sendDynamicText(4, "Test 123 öäüß");
|
||||
|
||||
verify(handler, times(2)).sendPck(byteBufferCaptor.capture());
|
||||
|
||||
String string1 = "GTDT41Test 123 ö";
|
||||
ByteBuffer chunk1 = ByteBuffer.allocate(stringToByteBuffer(string1).length + 1);
|
||||
chunk1.put(stringToByteBuffer(string1));
|
||||
chunk1.put((byte) -61); // first byte of ä
|
||||
|
||||
ByteBuffer chunk2 = ByteBuffer.allocate(18);
|
||||
chunk2.put(stringToByteBuffer("GTDT42"));
|
||||
chunk2.put((byte) -92); // second byte of ä
|
||||
chunk2.put(stringToByteBuffer("üß\0\0\0\0\0\0"));
|
||||
|
||||
assertThat(byteBufferCaptor.getAllValues(), contains(chunk1.array(), chunk2.array()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendKeysInvalidTable() throws LcnException {
|
||||
a.hitKey("E", 3, "MAKE");
|
||||
verify(handler, times(0)).sendPck(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendKeysNullTable() throws LcnException {
|
||||
a.hitKey(null, 3, "MAKE");
|
||||
verify(handler, times(0)).sendPck(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendKeysNullAction() throws LcnException {
|
||||
a.hitKey("D", 3, null);
|
||||
verify(handler, times(0)).sendPck(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendKeysInvalidKey0() throws LcnException {
|
||||
a.hitKey("D", 0, "MAKE");
|
||||
verify(handler, times(0)).sendPck(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendKeysInvalidKey9() throws LcnException {
|
||||
a.hitKey("D", 9, "MAKE");
|
||||
verify(handler, times(0)).sendPck(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendKeysInvalidAction() throws LcnException {
|
||||
a.hitKey("D", 8, "invalid");
|
||||
verify(handler, times(0)).sendPck(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendKeysA1Hit() throws LcnException {
|
||||
a.hitKey("a", 1, "HIT");
|
||||
|
||||
verify(handler).sendPck("TSK--10000000");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendKeysC8Hit() throws LcnException {
|
||||
a.hitKey("C", 8, "break");
|
||||
|
||||
verify(handler).sendPck("TS--O00000001");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendKeysD3Make() throws LcnException {
|
||||
a.hitKey("D", 3, "MAKE");
|
||||
|
||||
verify(handler).sendPck("TS---L00100000");
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.pchkdiscovery;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Test class for {@link LcnPchkDiscoveryService}.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnPchkDiscoveryServiceTest {
|
||||
private LcnPchkDiscoveryService s = new LcnPchkDiscoveryService();
|
||||
private ServicesResponse r = s.xmlToServiceResponse(RESPONSE);
|
||||
private static final String RESPONSE = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><ServicesResponse xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"servicesresponse.xsd\"><Version major=\"1\" minor=\"0\" /><Server requestId=\"1548\" machineId=\"b8:27:eb:fe:a4:bb\" machineName=\"raspberrypi\" osShort=\"Unix/Linux\" osLong=\"Unix/Linux\">LCN-PCHK 3.2.2 running on Unix/Linux</Server><Services /><ExtServices><ExtService name=\"LcnPchkBus\" major=\"1\" minor=\"0\" prot=\"TCP\" localPort=\"4114\">PCHK 3.2.2 bus</ExtService></ExtServices></ServicesResponse>";
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
s = new LcnPchkDiscoveryService();
|
||||
r = s.xmlToServiceResponse(RESPONSE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testXmlMachineId() {
|
||||
assertThat(r.getServer().getMachineId(), is("b8:27:eb:fe:a4:bb"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testXmlMachineName() {
|
||||
assertThat(r.getServer().getMachineName(), is("raspberrypi"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testXmlServerContent() {
|
||||
assertThat(r.getServer().getContent(), is("LCN-PCHK 3.2.2 running on Unix/Linux"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testXmlPort() {
|
||||
assertThat(r.getExtServices().getExtService().getLocalPort(), is(4114));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.lcn.internal.subhandler;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.openhab.binding.lcn.internal.LcnModuleHandler;
|
||||
import org.openhab.binding.lcn.internal.connection.ModInfo;
|
||||
|
||||
/**
|
||||
* Test class.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AbstractTestLcnModuleSubHandler {
|
||||
@Mock
|
||||
protected @NonNullByDefault({}) LcnModuleHandler handler;
|
||||
@Mock
|
||||
protected @NonNullByDefault({}) ModInfo info;
|
||||
|
||||
public AbstractTestLcnModuleSubHandler() {
|
||||
setUp();
|
||||
}
|
||||
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
when(handler.isMyAddress("000", "005")).thenReturn(true);
|
||||
}
|
||||
}
|
||||
@@ -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.lcn.internal.subhandler;
|
||||
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
|
||||
/**
|
||||
* Test class.
|
||||
*
|
||||
* @author Fabian Wolter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LcnModuleBinarySensorSubHandlerTest extends AbstractTestLcnModuleSubHandler {
|
||||
private @NonNullByDefault({}) LcnModuleBinarySensorSubHandler l;
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setUp() {
|
||||
super.setUp();
|
||||
|
||||
l = new LcnModuleBinarySensorSubHandler(handler, info);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStatusAllClosed() {
|
||||
l.tryParse("=M000005Bx000");
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "1", OpenClosedType.CLOSED);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "2", OpenClosedType.CLOSED);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "3", OpenClosedType.CLOSED);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "4", OpenClosedType.CLOSED);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "5", OpenClosedType.CLOSED);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "6", OpenClosedType.CLOSED);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "7", OpenClosedType.CLOSED);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "8", OpenClosedType.CLOSED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStatusAllOpen() {
|
||||
l.tryParse("=M000005Bx255");
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "1", OpenClosedType.OPEN);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "2", OpenClosedType.OPEN);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "3", OpenClosedType.OPEN);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "5", OpenClosedType.OPEN);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "6", OpenClosedType.OPEN);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "7", OpenClosedType.OPEN);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "8", OpenClosedType.OPEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStatus1And7Closed() {
|
||||
l.tryParse("=M000005Bx065");
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "1", OpenClosedType.OPEN);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "2", OpenClosedType.CLOSED);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "3", OpenClosedType.CLOSED);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "4", OpenClosedType.CLOSED);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "5", OpenClosedType.CLOSED);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "6", OpenClosedType.CLOSED);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "7", OpenClosedType.OPEN);
|
||||
verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "8", OpenClosedType.CLOSED);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user