diff --git a/CODEOWNERS b/CODEOWNERS index 6a373dde13819..86ceedf781144 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -96,6 +96,7 @@ /bundles/org.openhab.binding.konnected/ @volfan6415 /bundles/org.openhab.binding.kostalinverter/ @cschneider /bundles/org.openhab.binding.lametrictime/ @syphr42 +/bundles/org.openhab.binding.lcn/ @fwolter /bundles/org.openhab.binding.leapmotion/ @kaikreuzer /bundles/org.openhab.binding.lghombot/ @FluBBaOfWard /bundles/org.openhab.binding.lgtvserial/ @fa2k diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index f0045d390a790..6c2e753ce8ba6 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -476,6 +476,11 @@ org.openhab.binding.lametrictime ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.lcn + ${project.version} + org.openhab.addons.bundles org.openhab.binding.leapmotion diff --git a/bundles/org.openhab.binding.lcn/.classpath b/bundles/org.openhab.binding.lcn/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.binding.lcn/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.lcn/.project b/bundles/org.openhab.binding.lcn/.project new file mode 100644 index 0000000000000..5302a4a4a785d --- /dev/null +++ b/bundles/org.openhab.binding.lcn/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.lcn + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.lcn/NOTICE b/bundles/org.openhab.binding.lcn/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.lcn/README.md b/bundles/org.openhab.binding.lcn/README.md new file mode 100644 index 0000000000000..6226424c7e662 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/README.md @@ -0,0 +1,594 @@ +# 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** + +## LCN Overview + +LCN modules and their connecting peripherals are explained in the following. + +### LCN Modules + +Active LCN components connected to the LCN bus are called *LCN modules*. +LCN modules are addressed by their numeric id: Valid range is 5..254 + +In larger buildings, a second topologic layer is added: *segments*. +Valid range is 5..128 or 0 (= no segments exist) or 3 (= target all segments) + +LCN modules within the **same** segment can be grouped: Valid range is 5..254 or 3 (= target all groups) + +### LCN Firmware Versions + +Each LCN module has a feature-set based on its firmware version. +This version is written as follows: \[year since 1990\]\[month\]\[day\] + +Each component is written in hexadecimal with 2 characters. Examples: + +- 090101 = 1. january 1990 +- 0D0C01 = 1. december 2003 +- 170206 = 6. feb. 2013 + +### LCN Dimmer Outputs + +LCN modules support 2 to 4 dimmer output ports (number depends on firmware version). +If the module hardware type doesn't feature physical dimmer outputs, the outputs can still be used as virtual. + +Status values are always in percent. +Modules since 170206 have a 0.5%-steps resolution. Older modules have a 2%-steps resolution. + +The time it takes the output port to reach its setpoint is called *ramp*. + +### LCN Variables + +LCN modules support: + +- 3 or 12 (since 170206) analog variables for general purpose +- 2 regulators with configurable setpoints +- 5 or 4x4 (since 170206) thresholds (trigger levels) +- 4 S0-input counters (since 170206, LCN-BU4L must be connected) + +### LCN Regulators (additions to variables) + +LCN modules have 2 regulators. +Each one has a setpoint and uses one variable as its value source. +A regulator can be locked, so that the target actuator keeps switched off, also if the value source is in control range. + +### LCN Thresholds + +LCN modules since firmware 170206 have 4 threshold registers. Each threshold register comprises 4 thresholds. + +A threshold register uses one variable as its value source (see [LCN Variables](#lcn-variables)). +Arbitrary LCN commands can be send into the bus, when the value-source falls below a threshold or exceeds one. +A threshold can be locked, so that the configured LCN command is not fired, also if the value source passes the threshold. + +### LCN Relays + +LCN modules support 8 relays. If no hardware relays are connected, the relays can still be used as virtual. + +### LCN Binary Sensors + +LCN modules support 8 binary sensors (e.g. motion detectors; hardware periphery must be connected). + +### LCN LEDs (legacy name: *lamps*) + +12x multi-state variables can be used for logic operations or visualization (hardware periphery must be connected). + +Values: OFF, ON, BLINK, FLICKER + +### LCN Logic Operations (legacy name: *sums*) + +4x multi-state variables each representing the result of a logic operation of the associated LEDs. + +Values: NOT (all LEDs off), OR (some LEDs on), AND (all LEDs on) + +### LCN Keys + +LCN keys are data-points in the module with bound commands. +LCN modules support 3 ("A-C") or 4 ("A-D") key-tables (number depends on firmware version). + +Each key-table holds 8 keys. Examples: A1, A7, D8 + +Each key has 3 command types: HIT(press), MAKE(long press), BREAK(long press release) + +These keys can be locked. The bound (LCN-)commands cannot be executed, then. + +### LCN Access Control & Remote Controls + +LCN can interface several transponder readers and finger print sensors, used for access control. + +Remote controls can not only be used for triggering commands, but also for access control, by evaluating the transmitted serial number. + +## 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 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). + +### Thing: 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 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.
+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 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 of LCN modules can be read out by openHAB. +To do so, simply start openHAB's discovery. + +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 the module's serial number. + +### 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](#thing-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](#thing-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 *Things* LCN module (`module`) and LCN group (`group`). 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:
+If a special command is needed, the [Hit Key](#hit-key) action (German: "Sende Taste") 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 | +| 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 | +| 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 | - | - | - | Not implemented | +| 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. + +### Transponder + +LCN transponder 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 "17B308349E", the item "M10_Relay7" is switched on: + +``` +rule "My Transponder" +when + Channel "lcn:module:b827ebfea4bb:17B308349E:code#transponder" triggered "12ABCD" +then + M10_Relay7.sendCommand(ON) +end +``` + +### 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:::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:17B3073D6A: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:::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:17B3073D6A:code#remotecontrolcode" triggered "AB1234:A3:HIT" or + Channel "lcn:module:b827ebfea4bb:17B3073D6A: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:17B4196847: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:17B4196847: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:17B4196847:output#1"[profile="lcn:output", controlOutputs12=true]} +Dimmer M10_Outputs12b {channel="lcn:module:b827ebfea4bb:17B4196847: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:17B4196847:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0]} // ramp only since firmware 180501 +Dimmer M10_OutputAll2 {channel="lcn:module:b827ebfea4bb:17B4196847:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0.25]} // ramp compatibility: all +Dimmer M10_OutputAll3 {channel="lcn:module:b827ebfea4bb:17B4196847: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
+*key* - The number of the key within the key table: 1-8
+*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:17B4196847") + 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
+*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:17B3073D6A") + 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
+*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)
+*ramp* - The duration/ramp of one flash: 0-2 (0=2sec 1=1sec 2=0.5sec)
+*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:17B4196847") + // output=1, depth=2=100%, ramp=0=2s, count=3 + actions.flickerOutput(1, 2, 0, 3) +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:17B4196847:output#1"} +Switch M10_Output2 {channel="lcn:module:b827ebfea4bb:17B4196847:output#2"[profile="lcn:output", ramp=0.25]} // with ramp of 0.25s (smallest value) +Dimmer M10_Output3 {channel="lcn:module:b827ebfea4bb:17B4196847: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:17B4196847:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0]} // ramp=0: only since firmware 180501 +Dimmer M10_OutputAll2 {channel="lcn:module:b827ebfea4bb:17B4196847:output#1"[profile="lcn:output", controlAllOutputs=true, ramp=0.25]} // ramp=0.25: compatibility: all firmwares +Dimmer M10_OutputAll3 {channel="lcn:module:b827ebfea4bb:17B4196847: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:17B4196847:output#1"[profile="lcn:output", controlOutputs12=true, ramp=0.25]} + +// Dimmer Outputs: RGB Control +Color M10_Color {channel="lcn:module:b827ebfea4bb:17B4196847:output#color"[profile="lcn:output"]} + +// Roller Shutter on Output 1+2 +Rollershutter M10_RollershutterOutput1 {channel="lcn:module:b827ebfea4bb:17B4196847:rollershutteroutput#1"} + +// Relays +Switch M10_Relay1 {channel="lcn:module:b827ebfea4bb:17B4196847:relay#1"} + +// Roller Shutter on Relays 1+2 +Rollershutter M10_RollershutterRelay1 {channel="lcn:module:b827ebfea4bb:17B4196847:rollershutterrelay#1"} + +// LEDs +String M10_LED1 {channel="lcn:module:b827ebfea4bb:17B4196847:led#1"} +String M10_LED2 {channel="lcn:module:b827ebfea4bb:17B4196847:led#2"} + +// Logic Operations (legacy name: "Sums") +String M10_Logic1 {channel="lcn:module:b827ebfea4bb:17B4196847:logic#1"} +String M10_Logic2 {channel="lcn:module:b827ebfea4bb:17B4196847: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:17B4196847: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%]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#1"} // Temperature in °C +Number:Temperature M10_Variable2 "[%.1f °F]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#2"} // Temperature in °F +Number M10_Variable3 "[%d ppm]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#3"} // Indoor air quality in ppm +Number M10_Variable4 "[%d lx]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#4"} // Illuminance in Lux +Number:Illuminance M10_Variable5 "[%.1f klx]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#5"} // Illuminance in kLux +Number M10_Variable6 "[%.1f mA]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#6"} // Electrical current in mA +Number M10_Variable7 "[%.1f V]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#7"} // Voltage in V +Number M10_Variable8 "[%.1f m/s]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#8"} // Wind speed in m/s +Number M10_Variable9 "[%.1f °]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#9"} // position of the sun (azimuth or elevation) in ° +Number M10_Variable10 "[%d W]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#10"} // Current power of an S0 input in W +Number:Power M10_Variable11 "[%.1f kW]" {channel="lcn:module:b827ebfea4bb:17B4196847:variable#11"} // Current power of an S0 input in kW + +// Regulators +Number:Temperature M10_R1VarSetpoint "[%.1f %unit%]" {channel="lcn:module:b827ebfea4bb:17B4196847:rvarsetpoint#1"} // Temperature in °C +Switch M10_R1VarLock {channel="lcn:module:b827ebfea4bb:17B4196847:rvarlock#1"} // Lock state of R1Var + +// Thresholds +Number:Temperature M10_ThresholdRegister1_Threshold1 "[%.1f %unit%]" {channel="lcn:module:b827ebfea4bb:17B4196847:thresholdregister1#1"} // Temperature in °C +Number:Temperature M10_ThresholdRegister4_Threshold2 "[%.1f %unit%]" {channel="lcn:module:b827ebfea4bb:17B4196847:thresholdregister4#2"} // Temperature in °C + +// S0 Counters +Number:Energy M10_S0Counter1 "[%.1f kWh]" {channel="lcn:module:b827ebfea4bb:17B4196847:s0input#1"} + +// Key Locks +Switch M10_KeyLockA1 {channel="lcn:module:b827ebfea4bb:17B4196847:keylocktablea#1"} +Switch M10_KeyLockD5 {channel="lcn:module:b827ebfea4bb:17B4196847: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" + } +} +``` diff --git a/bundles/org.openhab.binding.lcn/doc/LCN-PRO_output_steps.png b/bundles/org.openhab.binding.lcn/doc/LCN-PRO_output_steps.png new file mode 100644 index 0000000000000..d86eaabe932fa Binary files /dev/null and b/bundles/org.openhab.binding.lcn/doc/LCN-PRO_output_steps.png differ diff --git a/bundles/org.openhab.binding.lcn/doc/dyn_text.png b/bundles/org.openhab.binding.lcn/doc/dyn_text.png new file mode 100644 index 0000000000000..64167c7b23ea3 Binary files /dev/null and b/bundles/org.openhab.binding.lcn/doc/dyn_text.png differ diff --git a/bundles/org.openhab.binding.lcn/doc/ir.png b/bundles/org.openhab.binding.lcn/doc/ir.png new file mode 100644 index 0000000000000..c287edb0eee96 Binary files /dev/null and b/bundles/org.openhab.binding.lcn/doc/ir.png differ diff --git a/bundles/org.openhab.binding.lcn/doc/overview.jpg b/bundles/org.openhab.binding.lcn/doc/overview.jpg new file mode 100644 index 0000000000000..f77334bdb2386 Binary files /dev/null and b/bundles/org.openhab.binding.lcn/doc/overview.jpg differ diff --git a/bundles/org.openhab.binding.lcn/pom.xml b/bundles/org.openhab.binding.lcn/pom.xml new file mode 100644 index 0000000000000..eccef96b93fe3 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.6-SNAPSHOT + + + org.openhab.binding.lcn + + openHAB Add-ons :: Bundles :: LCN Binding + + diff --git a/bundles/org.openhab.binding.lcn/src/main/feature/feature.xml b/bundles/org.openhab.binding.lcn/src/main/feature/feature.xml new file mode 100644 index 0000000000000..4b803e077eff1 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.lcn/${project.version} + + diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/DimmerOutputProfile.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/DimmerOutputProfile.java new file mode 100644 index 0000000000000..5c7feb50d2c78 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/DimmerOutputProfile.java @@ -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.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.thing.profiles.ProfileCallback; +import org.eclipse.smarthome.core.thing.profiles.ProfileContext; +import org.eclipse.smarthome.core.thing.profiles.ProfileTypeUID; +import org.eclipse.smarthome.core.thing.profiles.StateProfile; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.lcn.internal.common.DimmerOutputCommand; +import org.openhab.binding.lcn.internal.common.LcnDefs; +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 ramp = getConfig(profileContext, "ramp"); + Optional allOutputs = getConfig(profileContext, "controlAllOutputs"); + Optional 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 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 + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/ILcnModuleActions.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/ILcnModuleActions.java new file mode 100644 index 0000000000000..a6f2871e44321 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/ILcnModuleActions.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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); +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnBindingConstants.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnBindingConstants.java new file mode 100644 index 0000000000000..39c7d2b4ba62a --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnBindingConstants.java @@ -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.eclipse.smarthome.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(?\\d{3})(?\\d{3})"; + /** LCN coding for ACK */ + public static final int CODE_ACK = -1; +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnChannelVariableConfiguration.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnChannelVariableConfiguration.java new file mode 100644 index 0000000000000..6c3713cbcfe21 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnChannelVariableConfiguration.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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"; +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnGroupConfiguration.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnGroupConfiguration.java new file mode 100644 index 0000000000000..165966cdc85a0 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnGroupConfiguration.java @@ -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; +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnGroupHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnGroupHandler.java new file mode 100644 index 0000000000000..5ce3f6bdfeac3 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnGroupHandler.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lcn.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Thing; +import org.openhab.binding.lcn.internal.common.LcnAddr; +import org.openhab.binding.lcn.internal.common.LcnAddrGrp; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * 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 LcnAddr getCommandAddress() throws LcnException { + LcnAddrGrp localAddress = groupAddress; + if (localAddress == null) { + throw new LcnException("LCN group address not set"); + } + return localAddress; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnHandlerFactory.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnHandlerFactory.java new file mode 100644 index 0000000000000..b66e61f9d7ddd --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnHandlerFactory.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.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 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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleActions.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleActions.java new file mode 100644 index 0000000000000..2dd7524a44f8f --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleActions.java @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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.eclipse.smarthome.core.thing.binding.ThingActions; +import org.eclipse.smarthome.core.thing.binding.ThingActionsScope; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +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.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()); + } + } + + 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 EcobeeActions"); + } + + /** 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); + } + + private LcnModuleHandler getHandler() throws LcnException { + LcnModuleHandler localModuleHandler = moduleHandler; + if (localModuleHandler != null) { + return localModuleHandler; + } else { + throw new LcnException("Handler not set"); + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleConfiguration.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleConfiguration.java new file mode 100644 index 0000000000000..aa845f77027ab --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleConfiguration.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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; +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleDiscoveryService.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleDiscoveryService.java new file mode 100644 index 0000000000000..8ab89ce32297b --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleDiscoveryService.java @@ -0,0 +1,264 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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.eclipse.smarthome.config.discovery.AbstractDiscoveryService; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.openhab.binding.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.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(?\\d{3})(?\\d{3}).N(?[1-2]{1})(?.*)"); + private static final String SEGMENT_ID = "segmentId"; + private static final String MODULE_ID = "moduleId"; + private static final String SERIAL_NUMBER = "serialNumber"; + 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 SUPPORTED_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(LcnBindingConstants.THING_TYPE_MODULE).collect(Collectors.toSet())); + private @Nullable PckGatewayHandler bridgeHandler; + private final Map> moduleNames = new HashMap<>(); + private final Map discoveryResultBuilders = new ConcurrentHashMap<>(); + private final List 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 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"); + ThingUID thingUid = new ThingUID(LcnBindingConstants.THING_TYPE_MODULE, bridgeUid, + serialNumber); + + Map properties = new HashMap<>(3); + properties.put(SEGMENT_ID, addr.getSegmentId()); + properties.put(MODULE_ID, addr.getModuleId()); + properties.put(SERIAL_NUMBER, serialNumber); + + DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid) + .withProperties(properties).withRepresentationProperty(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 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 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 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(); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleHandler.java new file mode 100644 index 0000000000000..32ea98c7e35b3 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnModuleHandler.java @@ -0,0 +1,363 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.HSBType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.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.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.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 CONVERTERS = new HashMap<>(); + private @Nullable LcnAddrMod moduleAddress; + private final Map subHandlers = new HashMap<>(); + private final List metadataSubHandlers = new ArrayList<>(); + private final Map converters = new HashMap<>(); + + static { + CONVERTERS.put("temperature", Converters.TEMPERATURE); + CONVERTERS.put("light", Converters.LIGHT); + CONVERTERS.put("co2", Converters.CO2); + CONVERTERS.put("current", Converters.CURRENT); + CONVERTERS.put("voltage", Converters.VOLTAGE); + CONVERTERS.put("angle", Converters.ANGLE); + 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 { + // 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 variable value converters + for (Channel channel : thing.getChannels()) { + Object unitObject = channel.getConfiguration().get("unit"); + Object parameterObject = channel.getConfiguration().get("parameter"); + + if (unitObject instanceof String) { + switch ((String) unitObject) { + case "power": + case "energy": + converters.put(channel.getUID(), new S0Converter(parameterObject)); + break; + default: + if (CONVERTERS.containsKey(unitObject)) { + converters.put(channel.getUID(), CONVERTERS.get(unitObject)); + } + break; + } + } + } + + // 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()); + } + } + + @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 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) { + subHandler.handleCommandUpDown((UpDownType) command, channelGroup, number.get()); + } 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 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); + } + + /** + * 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> 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(); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnProfileFactory.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnProfileFactory.java new file mode 100644 index 0000000000000..a63c73e0c74cf --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/LcnProfileFactory.java @@ -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.eclipse.smarthome.core.library.CoreItemFactory; +import org.eclipse.smarthome.core.thing.profiles.Profile; +import org.eclipse.smarthome.core.thing.profiles.ProfileCallback; +import org.eclipse.smarthome.core.thing.profiles.ProfileContext; +import org.eclipse.smarthome.core.thing.profiles.ProfileFactory; +import org.eclipse.smarthome.core.thing.profiles.ProfileType; +import org.eclipse.smarthome.core.thing.profiles.ProfileTypeBuilder; +import org.eclipse.smarthome.core.thing.profiles.ProfileTypeProvider; +import org.eclipse.smarthome.core.thing.profiles.ProfileTypeUID; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +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 getSupportedProfileTypeUIDs() { + return Collections.singleton(DimmerOutputProfile.UID); + } + + @Override + public Collection 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; + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/PckGatewayConfiguration.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/PckGatewayConfiguration.java new file mode 100644 index 0000000000000..593396a463c2a --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/PckGatewayConfiguration.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/PckGatewayHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/PckGatewayHandler.java new file mode 100644 index 0000000000000..9d239c5936a02 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/PckGatewayHandler.java @@ -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.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.eclipse.smarthome.core.types.Command; +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.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> 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> 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 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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/DimmerOutputCommand.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/DimmerOutputCommand.java new file mode 100644 index 0000000000000..b54dd12367647 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/DimmerOutputCommand.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lcn.internal.common; + +import java.math.BigDecimal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddr.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddr.java new file mode 100644 index 0000000000000..c5630ecc69d9c --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddr.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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(); +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddrGrp.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddrGrp.java new file mode 100644 index 0000000000000..0873dfd0edd85 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddrGrp.java @@ -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 { + 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"; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddrMod.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddrMod.java new file mode 100644 index 0000000000000..81fe842230c38 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnAddrMod.java @@ -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 { + 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"; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnChannelGroup.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnChannelGroup.java new file mode 100644 index 0000000000000..d9d6a6da4f38c --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnChannelGroup.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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.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); + + private int count; + private BiFunction handlerFactory; + + private LcnChannelGroup(int count, + BiFunction 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); + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnDefs.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnDefs.java new file mode 100644 index 0000000000000..6ad123bc8affe --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnDefs.java @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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 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 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 { + HIT, + MAKE, + BREAK, + DONTSEND + } + + /** 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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnException.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnException.java new file mode 100644 index 0000000000000..3731bf000b6a3 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnException.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/PckGenerator.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/PckGenerator.java new file mode 100644 index 0000000000000..5c76e562639e3 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/PckGenerator.java @@ -0,0 +1,780 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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. + *

+ * 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 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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/ReverseNumber.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/ReverseNumber.java new file mode 100644 index 0000000000000..70a6c1fc82bd8 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/ReverseNumber.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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]; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/Variable.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/Variable.java new file mode 100644 index 0000000000000..c32573988d5ee --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/Variable.java @@ -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 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 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 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 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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/VariableValue.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/VariableValue.java new file mode 100644 index 0000000000000..90c6bf83c8cb6 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/VariableValue.java @@ -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.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.types.State; + +/** + * A value of an LCN variable. + *

+ * 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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionState.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionState.java new file mode 100644 index 0000000000000..13d001868090d --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionState.java @@ -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 { + /** 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 + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionStateSendCredentials.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionStateSendCredentials.java new file mode 100644 index 0000000000000..4037bf47e29ca --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionStateSendCredentials.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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)); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractState.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractState.java new file mode 100644 index 0000000000000..0b8bbd5a5205b --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractState.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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 type of the state machine implementation + * @param type of the state implementation + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public abstract class AbstractState, U extends AbstractState> { + private final List> 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 newStateFactory) { + synchronized (context) { + if (context.isStateActive(this)) { + context.setState(newStateFactory); + } + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractStateMachine.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractStateMachine.java new file mode 100644 index 0000000000000..fbf44830c440e --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractStateMachine.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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 type of the state machine implementation + * @param type of the state implementation + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public abstract class AbstractStateMachine, U extends AbstractState> { + 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 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 + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/Connection.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/Connection.java new file mode 100644 index 0000000000000..f89a7051c1e8c --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/Connection.java @@ -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: + *

    + *
  • Reconnection on connection loss + *
  • Segment scan (to detect the local segment ID) + *
  • Acknowledge handling + *
  • Periodic value requests + *
  • Caching of runtime data about the underlying LCN bus + *
+ * + * @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 offlineSendQueue = new LinkedBlockingQueue<>(); + private final Map 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 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)); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionCallback.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionCallback.java new file mode 100644 index 0000000000000..0a2b116250453 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionCallback.java @@ -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); +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionSettings.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionSettings.java new file mode 100644 index 0000000000000..dae04ef589fe6 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionSettings.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnected.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnected.java new file mode 100644 index 0000000000000..55acca37974b0 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnected.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnecting.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnecting.java new file mode 100644 index 0000000000000..35f54d8b22795 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnecting.java @@ -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 + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateGracePeriodBeforeReconnect.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateGracePeriodBeforeReconnect.java new file mode 100644 index 0000000000000..e6d05cba3ff49 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateGracePeriodBeforeReconnect.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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 + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateInit.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateInit.java new file mode 100644 index 0000000000000..a4c3790ee96a2 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateInit.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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 + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateMachine.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateMachine.java new file mode 100644 index 0000000000000..9e83f7ffeaa3b --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateMachine.java @@ -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 { + 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(); + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSegmentScan.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSegmentScan.java new file mode 100644 index 0000000000000..0942f777d85b9 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSegmentScan.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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(?\\d{3})(?\\d{3})\\.SK(?\\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); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendDimMode.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendDimMode.java new file mode 100644 index 0000000000000..bfcf89f37a766 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendDimMode.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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 + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendPassword.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendPassword.java new file mode 100644 index 0000000000000..01ae13bd3fe72 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendPassword.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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); + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendUsername.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendUsername.java new file mode 100644 index 0000000000000..a04f1d341a3c3 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendUsername.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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); + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateShutdown.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateShutdown.java new file mode 100644 index 0000000000000..67c7ff2100e25 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateShutdown.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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 + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnected.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnected.java new file mode 100644 index 0000000000000..8882042cebed4 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnected.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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")); + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnectedAfterDisconnected.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnectedAfterDisconnected.java new file mode 100644 index 0000000000000..8174ada6eecb7 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnectedAfterDisconnected.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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 + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ModInfo.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ModInfo.java new file mode 100644 index 0000000000000..a7799240c2e3f --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ModInfo.java @@ -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. + *
    + *
  • Stores the module's firmware version (if requested) + *
  • Manages the scheduling of status-requests + *
  • Manages the scheduling of acknowledged commands + *
+ * + * @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 requestStatusVars = new HashMap<>(); + + /** + * Caches the values of the variables, needed for changing the values. + */ + private final Map 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 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 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(); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/PckQueueItem.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/PckQueueItem.java new file mode 100644 index 0000000000000..f65ac8fa9ce9e --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/PckQueueItem.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/RequestStatus.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/RequestStatus.java new file mode 100644 index 0000000000000..e5b95e876bda8 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/RequestStatus.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendData.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendData.java new file mode 100644 index 0000000000000..a271f6a7902c8 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendData.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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; +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPck.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPck.java new file mode 100644 index 0000000000000..f04b9688d8489 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPck.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPlainText.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPlainText.java new file mode 100644 index 0000000000000..65e31e8f2d903 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPlainText.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converter.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converter.java new file mode 100644 index 0000000000000..456bbe847da48 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converter.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for all LCN variable value converters. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class Converter { + private final Logger logger = LoggerFactory.getLogger(Converter.class); + private @Nullable final Unit unit; + private final Function toHuman; + private final Function toNative; + + public Converter(@Nullable Unit unit, Function toHuman, Function 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 + */ + 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 + */ + 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 + */ + 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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converters.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converters.java new file mode 100644 index 0000000000000..3a8c5fff7aa04 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converters.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lcn.internal.converter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.eclipse.smarthome.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 Converter(SIUnits.CELSIUS, n -> (n - 1000) / 10d, h -> Math.round(h * 10) + 1000); + LIGHT = new Converter(SmartHomeUnits.LUX, Converters::lightToHumanReadable, Converters::lightToNative); + CO2 = new Converter(SmartHomeUnits.PARTS_PER_MILLION, n -> (double) n, Math::round); + CURRENT = new Converter(SmartHomeUnits.AMPERE, n -> n / 100d, h -> Math.round(h * 100)); + VOLTAGE = new Converter(SmartHomeUnits.VOLT, n -> n / 400d, h -> Math.round(h * 400)); + ANGLE = new Converter(SmartHomeUnits.DEGREE_ANGLE, n -> (n - 1000) / 10d, Converters::angleToNative); + WINDSPEED = new Converter(SmartHomeUnits.METRE_PER_SECOND, n -> n / 10d, h -> Math.round(h * 10)); + IDENTITY = new Converter(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); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/S0Converter.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/S0Converter.java new file mode 100644 index 0000000000000..b2b4989a668c8 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/S0Converter.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lcn.internal.converter; + +import java.math.BigDecimal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.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 Converter { + 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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtService.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtService.java new file mode 100644 index 0000000000000..1cf6f92bd24f0 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtService.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtServices.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtServices.java new file mode 100644 index 0000000000000..da2ec561faa8c --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtServices.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryService.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryService.java new file mode 100644 index 0000000000000..be8bb3fbc0925 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryService.java @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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.eclipse.smarthome.config.discovery.AbstractDiscoveryService; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.openhab.binding.lcn.internal.LcnBindingConstants; +import org.openhab.binding.lcn.internal.common.LcnDefs; +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 SUPPORTED_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(LcnBindingConstants.THING_TYPE_PCK_GATEWAY).collect(Collectors.toSet())); + private static final String DISCOVER_REQUEST = "openHAB"; + + public LcnPchkDiscoveryService() throws IllegalArgumentException { + super(SUPPORTED_THING_TYPES_UIDS, 0, false); + } + + private List getLocalAddresses() { + List 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 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); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Server.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Server.java new file mode 100644 index 0000000000000..ee39fa6ab7254 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Server.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ServicesResponse.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ServicesResponse.java new file mode 100644 index 0000000000000..e2a29e2434405 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ServicesResponse.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Version.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Version.java new file mode 100644 index 0000000000000..6c406662474ae --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Version.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleSubHandler.java new file mode 100644 index 0000000000000..3988283d50305 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleSubHandler.java @@ -0,0 +1,178 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.HSBType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.State; +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.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) 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 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; + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleVariableSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleVariableSubHandler.java new file mode 100644 index 0000000000000..5589d1d258197 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleVariableSubHandler.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lcn.internal.subhandler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +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.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; + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/ILcnModuleSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/ILcnModuleSubHandler.java new file mode 100644 index 0000000000000..aff33cc1f0ebd --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/ILcnModuleSubHandler.java @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.HSBType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.openhab.binding.lcn.internal.common.DimmerOutputCommand; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * 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 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 + * @throws LcnException when the command could not processed + */ + void handleCommandUpDown(UpDownType 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 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; +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandler.java new file mode 100644 index 0000000000000..a911662d25cb0 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandler.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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.eclipse.smarthome.core.library.types.OpenClosedType; +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 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(?\\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 getPckStatusMessagePatterns() { + return Collections.singleton(PATTERN); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleCodeSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleCodeSubHandler.java new file mode 100644 index 0000000000000..69a9102013a56 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleCodeSubHandler.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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(?\\d{3})(?\\d{3})(?\\d{3})"); + private static final Pattern REMOTE_CONTROL_PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + + "\\.ZI(?\\d{3})(?\\d{3})(?\\d{3})(?\\d{3})(?\\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 = 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() == 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 getPckStatusMessagePatterns() { + return Arrays.asList(TRANSPONDER_PATTERN, REMOTE_CONTROL_PATTERN); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandler.java new file mode 100644 index 0000000000000..b05116476e3cd --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandler.java @@ -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.eclipse.smarthome.core.library.types.OnOffType; +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.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(?\\d{3})(?\\d{3})(?\\d{3})((?\\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 getPckStatusMessagePatterns() { + return Collections.singleton(PATTERN); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandler.java new file mode 100644 index 0000000000000..6fd5d95cefa9f --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandler.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.StringType; +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; + +/** + * 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 getPckStatusMessagePatterns() { + return Collections.emptyList(); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandler.java new file mode 100644 index 0000000000000..21b5403ae16b1 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandler.java @@ -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.eclipse.smarthome.core.library.types.StringType; +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.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(?\\d{1})(?\\d{3})"); + private static final Pattern PATTERN_ALL = Pattern + .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.TL(?[AEBF]{12})(?[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 getPckStatusMessagePatterns() { + return Arrays.asList(PATTERN_ALL, PATTERN_SINGLE_LOGIC); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaAckSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaAckSubHandler.java new file mode 100644 index 0000000000000..6ced1c8c35699 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaAckSubHandler.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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(?\\d{3})(?\\d{3})!"); + private static final Pattern PATTERN_NEG = Pattern.compile("-M(?\\d{3})(?\\d{3})(?\\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 getPckStatusMessagePatterns() { + return Arrays.asList(PATTERN_POS, PATTERN_NEG); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaFirmwareSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaFirmwareSubHandler.java new file mode 100644 index 0000000000000..59621e8e1261f --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaFirmwareSubHandler.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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(?[0-9|A-F]{10})(?[0-9|A-F]{2})FW(?[0-9|A-F]{6})HW(?\\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)); + } + + @Override + public Collection getPckStatusMessagePatterns() { + return Collections.singleton(PATTERN); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandler.java new file mode 100644 index 0000000000000..d7f5d1fcb179c --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandler.java @@ -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.eclipse.smarthome.core.library.types.HSBType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.UpDownType; +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.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(?\\d)(?\\d+)"); + NATIVE_PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + "O(?\\d)(?\\d+)"); + } + + @Override + public Collection 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); + } + } + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandler.java new file mode 100644 index 0000000000000..d2b166cb67adc --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandler.java @@ -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.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.UpDownType; +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; + +/** + * 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(?\\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 getPckStatusMessagePatterns() { + return Collections.singleton(PATTERN); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandler.java new file mode 100644 index 0000000000000..71b7521ebcd93 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandler.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.UpDownType; +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; + +/** + * 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) 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) { + // 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 getPckStatusMessagePatterns() { + return Collections.emptyList(); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandler.java new file mode 100644 index 0000000000000..ef46add8a5f01 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandler.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.UpDownType; +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; + +/** + * 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) throws LcnException { + RelayStateModifier[] relayStateModifiers = createRelayStateModifierArray(); + // direction relay + relayStateModifiers[number * 2 + 1] = command == UpDownType.DOWN ? 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 getPckStatusMessagePatterns() { + return Collections.emptyList(); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandler.java new file mode 100644 index 0000000000000..a2611cc38213c --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandler.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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.eclipse.smarthome.core.library.types.OnOffType; +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; + +/** + * 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 getPckStatusMessagePatterns() { + return Collections.emptyList(); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandler.java new file mode 100644 index 0000000000000..5bf2d0c29c79d --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandler.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +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; + +/** + * 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(?\\d)(?\\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 getPckStatusMessagePatterns() { + return Collections.singleton(PATTERN); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandler.java new file mode 100644 index 0000000000000..428b8352f822c --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandler.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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.eclipse.smarthome.core.library.types.DecimalType; +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; + +/** + * 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(?\\d)(?\\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 getPckStatusMessagePatterns() { + return Collections.singleton(PATTERN); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandler.java new file mode 100644 index 0000000000000..744b61db88f9f --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandler.java @@ -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.eclipse.smarthome.core.library.types.DecimalType; +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.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(?\\d)(?\\d)(?\\d+)"); + private static final Pattern PATTERN_BEFORE_2013 = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + + "\\.S1(?\\d{5})(?\\d{5})(?\\d{5})(?\\d{5})(?\\d{5})(?\\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 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 getPckStatusMessagePatterns() { + return Arrays.asList(PATTERN, PATTERN_BEFORE_2013); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandler.java new file mode 100644 index 0000000000000..ac1ce6f3cc945 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandler.java @@ -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.eclipse.smarthome.core.library.types.DecimalType; +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.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(?\\d{3})(?\\d+)"); + private static final Pattern PATTERN_LEGACY = Pattern + .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.(?\\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 getPckStatusMessagePatterns() { + return Arrays.asList(PATTERN, PATTERN_LEGACY); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..c494e66acc295 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + LCN Binding + This is the binding for Local Control Network (LCN) + Fabian Wolter + + diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/config/config.xml b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/config/config.xml new file mode 100644 index 0000000000000..439256829226b --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/config/config.xml @@ -0,0 +1,102 @@ + + + + + + + The hostname or the IP address of the PCK gateway + network-address + true + + + + The IP port of the PCK gateway + 4114 + true + + + + The login username of the PCK gateway + true + + + + The login password of the PCK gateway + password + + + + IMPORTANT: Dimming range of all modules. Must be the same value as configured in LCN-PRO (Options/Settings/Expert Settings). If you only have modules with firmware newer than Feb. 2013, you probably want to choose 0 - 200.]]> + native200 + + + + + true + + + + Period after which an LCN command is resent, when no acknowledge has been received (in ms). + 3500 + true + + + + + + + The module ID, configured in LCN-PRO + + + + The segment ID the module is in (0 if no segments are present) + + + + + + + The group number, configured in LCN-PRO + + + + 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. + + + + The segment ID of all modules in this group (0 if no segments are present) + 0 + + + + + + + Unit of the sensor + native + + + + + + + + + + + + + true + + + + Only for S0 counters (power or energy) + 1000 + + + diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/i18n/lcn_de.properties b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/i18n/lcn_de.properties new file mode 100644 index 0000000000000..29ca7960c205e --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/i18n/lcn_de.properties @@ -0,0 +1,171 @@ +# 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 = WICHTIG: Der Dimmbereich von allen LCN-Modulen. Muss der gleiche Wert, wie in LCN-PRO sein (Optionen/Einstellungen/Experteneinstellungen). Wenn nur 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.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 diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..7921cd5534715 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/thing/thing-types.xml @@ -0,0 +1,634 @@ + + + + + + An LCN gateway speaking the PCK language. E.g. LCN-PCHK software or the DIN rail device LCN-PKE. + + + + + + + + + + + An LCN bus module, e.g. LCN-UPP, LCN-SH, LCN-HU + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An LCN group with multiple modules, configured in LCN-PRO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dimmer + + veto + + + + Color + + veto + + + + + + + + + + + + + + + + + + + + + + + + + Switch + + veto + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Rollershutter + + veto + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Contact + + veto + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Switch + + veto + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Only before Feb. 2013 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Switch + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + trigger + + + + + + trigger + + + + + + trigger + + + + + + trigger + + + + diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/ModuleActionsTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/ModuleActionsTest.java new file mode 100644 index 0000000000000..2867607cc6d90 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/ModuleActionsTest.java @@ -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 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"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryServiceTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryServiceTest.java new file mode 100644 index 0000000000000..38feefcc26362 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryServiceTest.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.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 = "LCN-PCHK 3.2.2 running on Unix/LinuxPCHK 3.2.2 bus"; + + @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)); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/AbstractTestLcnModuleSubHandler.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/AbstractTestLcnModuleSubHandler.java new file mode 100644 index 0000000000000..7cee0058d48b5 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/AbstractTestLcnModuleSubHandler.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandlerTest.java new file mode 100644 index 0000000000000..ba6f4a2581cf8 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandlerTest.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OpenClosedType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; + +/** + * 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); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandlerTest.java new file mode 100644 index 0000000000000..011bc56801970 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandlerTest.java @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleKeyLockTableSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleKeyLockTableSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleKeyLockTableSubHandler(handler, info); + } + + @Test + public void testStatus() { + l.tryParse("=M000005.TX098036000255"); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "1", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "2", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "3", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "4", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "5", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "6", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "7", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "8", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "1", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "2", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "3", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "4", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "5", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "6", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "7", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "8", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "1", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "2", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "3", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "4", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "5", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "6", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "7", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "8", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "1", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "2", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "3", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "4", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "5", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "6", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "7", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "8", OnOffType.ON); + } + + @Test + public void testHandleCommandA1Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEA, 0); + verify(handler).sendPck("TXA0-------"); + } + + @Test + public void testHandleCommandA1On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEA, 0); + verify(handler).sendPck("TXA1-------"); + } + + @Test + public void testHandleCommandA8Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEA, 7); + verify(handler).sendPck("TXA-------0"); + } + + @Test + public void testHandleCommandA8On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEA, 7); + verify(handler).sendPck("TXA-------1"); + } + + @Test + public void testHandleCommandB1Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEB, 0); + verify(handler).sendPck("TXB0-------"); + } + + @Test + public void testHandleCommandB1On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEB, 0); + verify(handler).sendPck("TXB1-------"); + } + + @Test + public void testHandleCommandB8Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEB, 7); + verify(handler).sendPck("TXB-------0"); + } + + @Test + public void testHandleCommandB8On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEB, 7); + verify(handler).sendPck("TXB-------1"); + } + + @Test + public void testHandleCommandC1Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEC, 0); + verify(handler).sendPck("TXC0-------"); + } + + @Test + public void testHandleCommandC1On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEC, 0); + verify(handler).sendPck("TXC1-------"); + } + + @Test + public void testHandleCommandC8Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEC, 7); + verify(handler).sendPck("TXC-------0"); + } + + @Test + public void testHandleCommandC8On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEC, 7); + verify(handler).sendPck("TXC-------1"); + } + + @Test + public void testHandleCommandD1Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLED, 0); + verify(handler).sendPck("TXD0-------"); + } + + @Test + public void testHandleCommandD1On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLED, 0); + verify(handler).sendPck("TXD1-------"); + } + + @Test + public void testHandleCommandD8Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLED, 7); + verify(handler).sendPck("TXD-------0"); + } + + @Test + public void testHandleCommandD8On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLED, 7); + verify(handler).sendPck("TXD-------1"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandlerTest.java new file mode 100644 index 0000000000000..aea07c1f19923 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandlerTest.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnDefs; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleLedSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleLedSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleLedSubHandler(handler, info); + } + + @Test + public void testHandleCommandLed1Off() throws LcnException { + l.handleCommandString(new StringType(LcnDefs.LedStatus.OFF.name()), 0); + verify(handler).sendPck("LA001A"); + } + + @Test + public void testHandleCommandLed1On() throws LcnException { + l.handleCommandString(new StringType(LcnDefs.LedStatus.ON.name()), 0); + verify(handler).sendPck("LA001E"); + } + + @Test + public void testHandleCommandLed1Blink() throws LcnException { + l.handleCommandString(new StringType(LcnDefs.LedStatus.BLINK.name()), 0); + verify(handler).sendPck("LA001B"); + } + + @Test + public void testHandleCommandLed1Flicker() throws LcnException { + l.handleCommandString(new StringType(LcnDefs.LedStatus.FLICKER.name()), 0); + verify(handler).sendPck("LA001F"); + } + + @Test + public void testHandleCommandLed12On() throws LcnException { + l.handleCommandString(new StringType(LcnDefs.LedStatus.ON.name()), 11); + verify(handler).sendPck("LA012E"); + } + + @Test + public void testHandleOnOffCommandLed1Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.LED, 0); + verify(handler).sendPck("LA001A"); + } + + @Test + public void testHandleOnOffCommandLed1On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.LED, 0); + verify(handler).sendPck("LA001E"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandlerTest.java new file mode 100644 index 0000000000000..106fd18f6d45d --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandlerTest.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.StringType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleLogicSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private static final StringType ON = new StringType("ON"); + private static final StringType OFF = new StringType("OFF"); + private static final StringType BLINK = new StringType("BLINK"); + private static final StringType FLICKER = new StringType("FLICKER"); + private static final StringType NOT = new StringType("NOT"); + private static final StringType OR = new StringType("OR"); + private static final StringType AND = new StringType("AND"); + private @NonNullByDefault({}) LcnModuleLogicSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleLogicSubHandler(handler, info); + } + + @Test + public void testStatusLedOffLogicNot() { + l.tryParse("=M000005.TLAAAAAAAAAAAANNNN"); + verify(handler).updateChannel(LcnChannelGroup.LED, "1", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "2", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "3", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "4", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "5", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "6", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "7", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "8", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "9", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "10", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "11", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "12", OFF); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "1", NOT); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "2", NOT); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "3", NOT); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "4", NOT); + } + + @Test + public void testStatusMixed() { + l.tryParse("=M000005.TLAEBFAAAAAAAFNVNT"); + verify(handler).updateChannel(LcnChannelGroup.LED, "1", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "2", ON); + verify(handler).updateChannel(LcnChannelGroup.LED, "3", BLINK); + verify(handler).updateChannel(LcnChannelGroup.LED, "4", FLICKER); + verify(handler).updateChannel(LcnChannelGroup.LED, "5", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "6", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "7", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "8", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "9", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "10", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "11", OFF); + verify(handler).updateChannel(LcnChannelGroup.LED, "12", FLICKER); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "1", NOT); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "2", AND); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "3", NOT); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "4", OR); + } + + @Test + public void testStatusSingleLogic1Not() { + l.tryParse("=M000005S1000"); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "1", NOT); + } + + @Test + public void testStatusSingleLogic4Or() { + l.tryParse("=M000005S4025"); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "4", OR); + } + + @Test + public void testStatusSingleLogic3And() { + l.tryParse("=M000005S3050"); + verify(handler).updateChannel(LcnChannelGroup.LOGIC, "3", AND); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandlerTest.java new file mode 100644 index 0000000000000..e18eebff250cc --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandlerTest.java @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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 java.math.BigDecimal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.junit.Before; +import org.junit.Test; +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; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleOutputSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleOutputSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleOutputSubHandler(handler, info); + } + + @Test + public void testStatusOutput1OffPercent() { + l.tryParse("=M000005A1000"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(0)); + } + + @Test + public void testStatusOutput2OffPercent() { + l.tryParse("=M000005A2000"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(0)); + } + + @Test + public void testStatusOutput1OffNative() { + l.tryParse("=M000005O1000"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(0)); + } + + @Test + public void testStatusOutput2OffNative() { + l.tryParse("=M000005O2000"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(0)); + } + + @Test + public void testStatusOutput1OnPercent() { + l.tryParse("=M000005A1100"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(100)); + } + + @Test + public void testStatusOutput2OnPercent() { + l.tryParse("=M000005A2100"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(100)); + } + + @Test + public void testStatusOutput1OnNative() { + l.tryParse("=M000005O1200"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(100)); + } + + @Test + public void testStatusOutput2OnNative() { + l.tryParse("=M000005O2200"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(100)); + } + + @Test + public void testStatusOutput2On50Percent() { + l.tryParse("=M000005A2050"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(50)); + } + + @Test + public void testStatusOutput1On50Native() { + l.tryParse("=M000005O1100"); + verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(50)); + } + + @Test + public void testHandleCommandOutput1On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.OUTPUT, 0); + verify(handler).sendPck("A1DI100000"); + } + + @Test + public void testHandleCommandOutput2On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.OUTPUT, 1); + verify(handler).sendPck("A2DI100000"); + } + + @Test + public void testHandleCommandOutput1Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.OUTPUT, 0); + verify(handler).sendPck("A1DI000000"); + } + + @Test + public void testHandleCommandOutput2Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.OUTPUT, 1); + verify(handler).sendPck("A2DI000000"); + } + + @Test + public void testHandleCommandOutput1Percent10() throws LcnException { + l.handleCommandPercent(new PercentType(99), LcnChannelGroup.OUTPUT, 0); + verify(handler).sendPck("A1DI099000"); + } + + @Test + public void testHandleCommandOutput2Percent1() throws LcnException { + l.handleCommandPercent(new PercentType(1), LcnChannelGroup.OUTPUT, 1); + verify(handler).sendPck("A2DI001000"); + } + + @Test + public void testHandleCommandOutput1Percent995() throws LcnException { + l.handleCommandPercent(new PercentType(BigDecimal.valueOf(99.5)), LcnChannelGroup.OUTPUT, 0); + verify(handler).sendPck("O1DI199000"); + } + + @Test + public void testHandleCommandOutput2Percent05() throws LcnException { + l.handleCommandPercent(new PercentType(BigDecimal.valueOf(0.5)), LcnChannelGroup.OUTPUT, 1); + verify(handler).sendPck("O2DI001000"); + } + + @Test + public void testHandleCommandDimmerOutputAll60FixedRamp() throws LcnException { + l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(60), true, false, LcnDefs.FIXED_RAMP_MS), + 0); + verify(handler).sendPck("AH060"); + } + + @Test + public void testHandleCommandDimmerOutputAll40CustomRamp() throws LcnException { + l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(40), true, false, 1000), 0); + verify(handler).sendPck("OY080080080080004"); + } + + @Test + public void testHandleCommandDimmerOutput12Value100FixedRamp() throws LcnException { + l.handleCommandDimmerOutput( + new DimmerOutputCommand(BigDecimal.valueOf(100), false, true, LcnDefs.FIXED_RAMP_MS), 0); + verify(handler).sendPck("X2001200200"); + } + + @Test + public void testHandleCommandDimmerOutput12Value0FixedRamp() throws LcnException { + l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(0), false, true, LcnDefs.FIXED_RAMP_MS), + 0); + verify(handler).sendPck("X2001000000"); + } + + @Test + public void testHandleCommandDimmerOutput12Value100NoRamp() throws LcnException { + l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(100), false, true, 0), 0); + verify(handler).sendPck("X2001253253"); + } + + @Test + public void testHandleCommandDimmerOutput12Value0NoRamp() throws LcnException { + l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(0), false, true, 0), 0); + verify(handler).sendPck("X2001252252"); + } + + @Test + public void testHandleCommandDimmerOutput12Value40() throws LcnException { + l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(40), false, true, 0), 0); + verify(handler).sendPck("AY040040"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandlerTest.java new file mode 100644 index 0000000000000..25c181309b02e --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandlerTest.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRelaySubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleRelaySubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleRelaySubHandler(handler, info); + } + + @Test + public void testStatusAllOff() { + l.tryParse("=M000005Rx000"); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "1", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "2", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "3", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "4", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "5", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "6", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "7", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "8", OnOffType.OFF); + } + + @Test + public void testStatusAllOn() { + l.tryParse("=M000005Rx255"); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "1", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "2", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "3", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "5", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "6", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "7", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "8", OnOffType.ON); + } + + @Test + public void testStatusRelay1Relay7On() { + l.tryParse("=M000005Rx065"); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "1", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "2", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "3", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "4", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "5", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "6", OnOffType.OFF); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "7", OnOffType.ON); + verify(handler).updateChannel(LcnChannelGroup.RELAY, "8", OnOffType.OFF); + } + + @Test + public void testHandleCommandRelay1On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.RELAY, 0); + verify(handler).sendPck("R81-------"); + } + + @Test + public void testHandleCommandRelay8On() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.RELAY, 7); + verify(handler).sendPck("R8-------1"); + } + + @Test + public void testHandleCommandRelay1Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.RELAY, 0); + verify(handler).sendPck("R80-------"); + } + + @Test + public void testHandleCommandRelay8Off() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.RELAY, 7); + verify(handler).sendPck("R8-------0"); + } + + @Test + public void testHandleCommandRelay8Percent1() throws LcnException { + l.handleCommandPercent(new PercentType(1), LcnChannelGroup.RELAY, 7); + verify(handler).sendPck("R8-------1"); + } + + @Test + public void testHandleCommandRelay1Percent0() throws LcnException { + l.handleCommandPercent(PercentType.ZERO, LcnChannelGroup.RELAY, 0); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandlerTest.java new file mode 100644 index 0000000000000..3809f0a099788 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandlerTest.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRollershutterOutputSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleRollershutterOutputSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleRollershutterOutputSubHandler(handler, info); + } + + @Test + public void testUp() throws LcnException { + l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0); + verify(handler).sendPck("A1DI100008"); + } + + @Test + public void testDown() throws LcnException { + l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0); + verify(handler).sendPck("A2DI100008"); + } + + @Test + public void testStop() throws LcnException { + l.handleCommandStopMove(StopMoveType.STOP, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0); + verify(handler).sendPck("A1DI000000"); + verify(handler).sendPck("A2DI000000"); + } + + @Test + public void testMove() throws LcnException { + l.handleCommandStopMove(StopMoveType.MOVE, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0); + verify(handler).sendPck("A2DI100008"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandlerTest.java new file mode 100644 index 0000000000000..99816dc13d055 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandlerTest.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRollershutterRelaySubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleRollershutterRelaySubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleRollershutterRelaySubHandler(handler, info); + } + + @Test + public void testUp1() throws LcnException { + l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTERRELAY, 0); + verify(handler).sendPck("R810------"); + } + + @Test + public void testUp4() throws LcnException { + l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTERRELAY, 3); + verify(handler).sendPck("R8------10"); + } + + @Test + public void testDown1() throws LcnException { + l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTERRELAY, 0); + verify(handler).sendPck("R811------"); + } + + @Test + public void testDown4() throws LcnException { + l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTERRELAY, 3); + verify(handler).sendPck("R8------11"); + } + + @Test + public void testStop1() throws LcnException { + l.handleCommandStopMove(StopMoveType.STOP, LcnChannelGroup.ROLLERSHUTTERRELAY, 0); + verify(handler).sendPck("R80-------"); + } + + @Test + public void testStop4() throws LcnException { + l.handleCommandStopMove(StopMoveType.STOP, LcnChannelGroup.ROLLERSHUTTERRELAY, 3); + verify(handler).sendPck("R8------0-"); + } + + @Test + public void testMove1() throws LcnException { + l.handleCommandStopMove(StopMoveType.MOVE, LcnChannelGroup.ROLLERSHUTTERRELAY, 0); + verify(handler).sendPck("R81-------"); + } + + @Test + public void testMove4() throws LcnException { + l.handleCommandStopMove(StopMoveType.MOVE, LcnChannelGroup.ROLLERSHUTTERRELAY, 3); + verify(handler).sendPck("R8------1-"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandlerTest.java new file mode 100644 index 0000000000000..8acf7e33e4c63 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandlerTest.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRvarLockSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleRvarLockSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleRvarLockSubHandler(handler, info); + } + + @Test + public void testLock1() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.RVARLOCK, 0); + verify(handler).sendPck("REAXS"); + } + + @Test + public void testLock2() throws LcnException { + l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.RVARLOCK, 1); + verify(handler).sendPck("REBXS"); + } + + @Test + public void testUnlock1() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.RVARLOCK, 0); + verify(handler).sendPck("REAXA"); + } + + @Test + public void testUnlock2() throws LcnException { + l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.RVARLOCK, 1); + verify(handler).sendPck("REBXA"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandlerTest.java new file mode 100644 index 0000000000000..be0d3df53395b --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandlerTest.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lcn.internal.subhandler; + +import static org.mockito.Mockito.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.Variable; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleRvarSetpointSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleRvarSetpointSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleRvarSetpointSubHandler(handler, info); + } + + @Test + public void testhandleCommandRvar1Positive() throws LcnException { + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(1000), LcnChannelGroup.RVARSETPOINT, 0); + verify(handler).sendPck("X2030032000"); + } + + @Test + public void testhandleCommandRvar2Positive() throws LcnException { + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.RVARSETPOINT, 1); + verify(handler).sendPck("X2030096100"); + } + + @Test + public void testhandleCommandRvar1Negative() throws LcnException { + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(0), LcnChannelGroup.RVARSETPOINT, 0); + verify(handler).sendPck("X2030043232"); + } + + @Test + public void testhandleCommandRvar2Negative() throws LcnException { + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(999), LcnChannelGroup.RVARSETPOINT, 1); + verify(handler).sendPck("X2030104001"); + } + + @Test + public void testhandleCommandRvar1PositiveLegacy() throws LcnException { + when(info.getVariableValue(Variable.RVARSETPOINT1)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.RVARSETPOINT, 0); + verify(handler).sendPck("REASA+100"); + } + + @Test + public void testhandleCommandRvar2PositiveLegacy() throws LcnException { + when(info.getVariableValue(Variable.RVARSETPOINT2)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.RVARSETPOINT, 1); + verify(handler).sendPck("REBSA+100"); + } + + @Test + public void testhandleCommandRvar1NegativeLegacy() throws LcnException { + when(info.getVariableValue(Variable.RVARSETPOINT1)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.RVARSETPOINT, 0); + verify(handler).sendPck("REASA-100"); + } + + @Test + public void testhandleCommandRvar2NegativeLegacy() throws LcnException { + when(info.getVariableValue(Variable.RVARSETPOINT2)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.RVARSETPOINT, 1); + verify(handler).sendPck("REBSA-100"); + } + + @Test + public void testRvar1() { + l.tryParse("=M000005.S11234"); + verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "1", new DecimalType(1234)); + verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "1", OnOffType.OFF); + } + + @Test + public void testRvar2() { + l.tryParse("=M000005.S21234"); + verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "2", new DecimalType(1234)); + verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "2", OnOffType.OFF); + } + + @Test + public void testRvar1SensorDefective() { + l.tryParse("=M000005.S132512"); + verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "1", new StringType("DEFECTIVE")); + verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "1", OnOffType.OFF); + } + + @Test + public void testRvar1Locked() { + l.tryParse("=M000005.S134002"); + verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "1", new DecimalType(1234)); + verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "1", OnOffType.ON); + } + + @Test + public void testRvar2Locked() { + l.tryParse("=M000005.S234002"); + verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "2", new DecimalType(1234)); + verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "2", OnOffType.ON); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandlerTest.java new file mode 100644 index 0000000000000..4f5f900c17e35 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandlerTest.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lcn.internal.subhandler; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleS0CounterSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleS0CounterSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleS0CounterSubHandler(handler, info); + } + + @Test + public void testZero() { + l.tryParse("=M000005.C10"); + verify(handler).updateChannel(LcnChannelGroup.S0INPUT, "1", new DecimalType(0)); + } + + @Test + public void testMaxValue() { + l.tryParse("=M000005.C14294967295"); + verify(handler).updateChannel(LcnChannelGroup.S0INPUT, "1", new DecimalType(4294967295L)); + } + + @Test + public void test4() { + l.tryParse("=M000005.C412345"); + verify(handler).updateChannel(LcnChannelGroup.S0INPUT, "4", new DecimalType(12345)); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandlerTest.java new file mode 100644 index 0000000000000..ad3ee3bb85ab5 --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandlerTest.java @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.Variable; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleThresholdSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleThresholdSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleThresholdSubHandler(handler, info); + } + + @Test + public void testThreshold11() { + l.tryParse("=M000005.T1112345"); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "1", new DecimalType(12345)); + } + + @Test + public void testThreshold14() { + l.tryParse("=M000005.T140"); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "4", new DecimalType(0)); + } + + @Test + public void testThreshold41() { + l.tryParse("=M000005.T4112345"); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER4, "1", new DecimalType(12345)); + } + + @Test + public void testThresholdLegacy() { + l.tryParse("=M000005.S1123451123411123000000000112345"); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "1", new DecimalType(12345)); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "2", new DecimalType(11234)); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "3", new DecimalType(11123)); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "4", new DecimalType(0)); + verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "5", new DecimalType(1)); + } + + @Test + public void testhandleCommandThreshold11Positive() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER11)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER1, 0); + verify(handler).sendPck("SSR0100AR11"); + } + + @Test + public void testhandleCommandThreshold11Negative() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER11)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.THRESHOLDREGISTER1, 0); + verify(handler).sendPck("SSR0100SR11"); + } + + @Test + public void testhandleCommandThreshold44Positive() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER44)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER4, 3); + verify(handler).sendPck("SSR0100AR44"); + } + + @Test + public void testhandleCommandThreshold44Negative() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER44)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(true); + l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.THRESHOLDREGISTER4, 3); + verify(handler).sendPck("SSR0100SR44"); + } + + @Test + public void testhandleCommandThreshold11LegacyPositive() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER11)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER1, 0); + verify(handler).sendPck("SSR0100A10000"); + } + + @Test + public void testhandleCommandThreshold11LegacyNegative() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER11)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.THRESHOLDREGISTER1, 0); + verify(handler).sendPck("SSR0100S10000"); + } + + @Test + public void testhandleCommandThreshold14Legacy() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER14)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER1, 3); + verify(handler).sendPck("SSR0100A00010"); + } + + @Test + public void testhandleCommandThreshold15Legacy() throws LcnException { + when(info.getVariableValue(Variable.THRESHOLDREGISTER15)).thenReturn(1000L); + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER1, 4); + verify(handler).sendPck("SSR0100A00001"); + } +} diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandlerTest.java new file mode 100644 index 0000000000000..de976ee4acb8d --- /dev/null +++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandlerTest.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.junit.Before; +import org.junit.Test; +import org.openhab.binding.lcn.internal.common.LcnChannelGroup; +import org.openhab.binding.lcn.internal.common.LcnException; +import org.openhab.binding.lcn.internal.common.Variable; + +/** + * Test class. + * + * @author Fabian Wolter - Initial contribution + */ +@NonNullByDefault +public class LcnModuleVariableSubHandlerTest extends AbstractTestLcnModuleSubHandler { + private @NonNullByDefault({}) LcnModuleVariableSubHandler l; + + @Override + @Before + public void setUp() { + super.setUp(); + + l = new LcnModuleVariableSubHandler(handler, info); + } + + @Test + public void testStatusVariable1() { + l.tryParse("=M000005.A00112345"); + verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "1", new DecimalType(12345)); + } + + @Test + public void testStatusVariable12() { + l.tryParse("=M000005.A01212345"); + verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "12", new DecimalType(12345)); + } + + @Test + public void testStatusLegacyVariable3() { + when(info.getLastRequestedVarWithoutTypeInResponse()).thenReturn(Variable.VARIABLE3); + l.tryParse("=M000005.12345"); + verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "3", new DecimalType(12345)); + } + + @Test + public void testHandleCommandLegacyTvarPositive() throws LcnException { + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + when(info.getVariableValue(Variable.VARIABLE1)).thenReturn(1000L); + l.handleCommandDecimal(new DecimalType(1234), LcnChannelGroup.VARIABLE, 0); + verify(handler).sendPck("ZA234"); + } + + @Test + public void testHandleCommandLegacyTvarNegative() throws LcnException { + when(info.hasExtendedMeasurementProcessing()).thenReturn(false); + when(info.getVariableValue(Variable.VARIABLE1)).thenReturn(2000L); + l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.VARIABLE, 0); + verify(handler).sendPck("ZS900"); + } + + @Test + public void testStatusVariable10SensorDefective() { + l.tryParse("=M000005.A01032512"); + verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "10", new StringType("DEFECTIVE")); + } + + @Test + public void testStatusVariable8NotConfigured() { + l.tryParse("=M000005.A00865535"); + verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "8", new StringType("Not configured in LCN-PRO")); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 83c0d00f65d47..dec0ca57a7b4e 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -132,6 +132,7 @@ org.openhab.binding.konnected org.openhab.binding.kostalinverter org.openhab.binding.lametrictime + org.openhab.binding.lcn org.openhab.binding.leapmotion org.openhab.binding.lghombot org.openhab.binding.lgtvserial