added migrated 2.x add-ons

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

View File

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

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.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>

View 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

View 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.
![Illustration of the LCN product family](doc/overview.jpg)
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.
![LCN-PRO screenshot, showing the 50 or 200 steps for the dimmer outputs](doc/LCN-PRO_output_steps.png)
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.
![Screenshot, showing the send keys command](doc/host_command_send_keys.png)
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*:
![Screenshot, showing the I-port properties for remote controls](doc/ir.png)
#### 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*:
![Screenshot of LCN-PRO, showing the dynamic text setting of an LCN-GT10D](doc/dyn_text.png)
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"
}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

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

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}

View File

@@ -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;
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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";
}

View File

@@ -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;
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}
}

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}
}

View File

@@ -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");
}
}
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}
}
}

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}
}

View File

@@ -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();
}

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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";
}
}

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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";
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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];
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,76 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}
}
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}

View File

@@ -0,0 +1,97 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}

View File

@@ -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
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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"));
}
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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());
}
}

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,179 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -0,0 +1,86 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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();
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,88 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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 &amp; 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>

View File

@@ -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");
}
}

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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));
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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